mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
refactor(share): stream shared place-photo bytes through storage
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Body, Controller, Delete, Get, HttpException, Param, Post, Res, UseGuards } from '@nestjs/common';
|
||||
import type { Response } from 'express';
|
||||
import { createReadStream } from 'node:fs';
|
||||
import type { Readable } from 'node:stream';
|
||||
import type { User } from '../../types';
|
||||
import { ShareService } from './share.service';
|
||||
import { StorageService } from '../storage/storage.service';
|
||||
import { ShareLinkDto } from './share.dto';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
@@ -78,7 +79,10 @@ export class TripShareController {
|
||||
@Public('share-token validated: a shared trip link has to work for somebody without an account')
|
||||
@Controller('api/shared')
|
||||
export class SharedController {
|
||||
constructor(private readonly share: ShareService) {}
|
||||
constructor(
|
||||
private readonly share: ShareService,
|
||||
private readonly storage: StorageService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Public, token-scoped place-photo proxy. The shared payload rewrites place
|
||||
@@ -89,15 +93,25 @@ export class SharedController {
|
||||
* MapsController.placePhotoBytes (cached photos are always JPEG).
|
||||
*/
|
||||
@Get(':token/place-photo/:placeId/bytes')
|
||||
placePhotoBytes(@Param('token') token: string, @Param('placeId') placeId: string, @Res() res: Response): void {
|
||||
const fp = this.share.getSharedPlacePhotoPath(token, placeId);
|
||||
if (!fp) {
|
||||
async placePhotoBytes(@Param('token') token: string, @Param('placeId') placeId: string, @Res() res: Response): Promise<void> {
|
||||
const key = this.share.getSharedPlacePhotoKey(token, placeId);
|
||||
if (!key) {
|
||||
res.status(404).json({ error: 'Photo not cached' });
|
||||
return;
|
||||
}
|
||||
// Bytes are stream-piped like the maps handler; headers go on before the
|
||||
// stream attempt so an early failure overrides them exactly as the old
|
||||
// createReadStream error event did.
|
||||
res.set('Cache-Control', 'public, max-age=2592000, immutable');
|
||||
res.type('image/jpeg');
|
||||
const stream = createReadStream(fp);
|
||||
let stream: Readable;
|
||||
try {
|
||||
({ stream } = await this.storage.getStream('photos-google', key));
|
||||
} catch {
|
||||
// Cache-delete race — same terminal state as the old stream error path.
|
||||
if (!res.headersSent) res.status(404).json({ error: 'Photo not cached' });
|
||||
return;
|
||||
}
|
||||
stream.on('error', () => {
|
||||
if (!res.headersSent) res.status(404).json({ error: 'Photo not cached' });
|
||||
});
|
||||
|
||||
@@ -6,11 +6,12 @@ import { SettingsModule } from '../settings/settings.module';
|
||||
import { PermissionsModule } from '../permissions/permissions.module';
|
||||
import { QueryHelpersModule } from '../query-helpers/query-helpers.module';
|
||||
import { PlacePhotosModule } from '../place-photos/place-photos.module';
|
||||
import { StorageModule } from '../storage/storage.module';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { McpSharedModule } from '../mcp-shared/mcp-shared.module';
|
||||
|
||||
@Module({
|
||||
imports: [McpSharedModule, SettingsModule, PermissionsModule, QueryHelpersModule, AuthModule, PlacePhotosModule],
|
||||
imports: [McpSharedModule, SettingsModule, PermissionsModule, QueryHelpersModule, AuthModule, PlacePhotosModule, StorageModule],
|
||||
controllers: [TripShareController, SharedController],
|
||||
providers: [ShareService, ShareMcp],
|
||||
})
|
||||
|
||||
@@ -297,14 +297,15 @@ export class ShareService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the on-disk path for a cached place photo requested through a public
|
||||
* share link. Validates that the token is valid + unexpired and that the place
|
||||
* actually belongs to that token's trip (matched via the stored proxy URL, which
|
||||
* covers both Google `placeId` and Wikimedia `coords:` pseudo-IDs without
|
||||
* depending on google_place_id). Returns null — never throws — so the caller
|
||||
* answers a plain 404, mirroring the authenticated bytes endpoint.
|
||||
* Resolves the storage name (category 'photos-google') for a cached place
|
||||
* photo requested through a public share link. Validates that the token is
|
||||
* valid + unexpired and that the place actually belongs to that token's trip
|
||||
* (matched via the stored proxy URL, which covers both Google `placeId` and
|
||||
* Wikimedia `coords:` pseudo-IDs without depending on google_place_id).
|
||||
* Returns null — never throws — so the caller answers a plain miss,
|
||||
* mirroring the authenticated bytes endpoint.
|
||||
*/
|
||||
getSharedPlacePhotoPath(token: string, placeId: string): string | null {
|
||||
getSharedPlacePhotoKey(token: string, placeId: string): string | null {
|
||||
const shareRow = this.dbs.get<{ trip_id: string; share_map: number }>(
|
||||
"SELECT trip_id, share_map FROM share_tokens WHERE token = ? AND (expires_at IS NULL OR expires_at > datetime('now'))",
|
||||
token,
|
||||
@@ -319,6 +320,6 @@ export class ShareService {
|
||||
const place = this.dbs.get('SELECT 1 FROM places WHERE trip_id = ? AND image_url = ?', shareRow.trip_id, expectedUrl);
|
||||
if (!place) return null;
|
||||
|
||||
return this.photoCache.serveFilePath(placeId);
|
||||
return this.photoCache.serveKey(placeId);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user