mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
fix(journey): serve local file when uploading photos with Immich sync enabled
After upload, trek_photos.provider is immediately flipped to 'immich' even though Immich's thumbnail generation is async. streamPhoto then routed to Immich, which returned an error for the not-yet-processed asset. Because Cache-Control was set before the proxy attempt, the error response was cached by the browser for 24h — breaking thumbnails until a hard refresh bypassed the cache and Immich had finished processing. - streamPhoto now prefers the local file_path when it exists on disk, regardless of provider; Immich/Synology are only used when no local file is available (fixes the immediate broken-thumbnail symptom) - pipeAsset sets Cache-Control: no-store on upstream errors and uses the caller-supplied default only on success (prevents cache poisoning) - streamImmichAsset no longer pre-sets Cache-Control before the proxy - streamSynologyAsset passes the same defaultCacheControl through pipeAsset Closes #691
This commit is contained in:
@@ -253,13 +253,19 @@ export function updateSyncTimeForAlbumLink(linkId: string): void {
|
|||||||
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
db.prepare('UPDATE trip_album_links SET last_synced_at = CURRENT_TIMESTAMP WHERE id = ?').run(linkId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pipeAsset(url: string, response: Response, headers?: Record<string, string>, signal?: AbortSignal): Promise<void> {
|
export async function pipeAsset(url: string, response: Response, headers?: Record<string, string>, signal?: AbortSignal, defaultCacheControl?: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const resp = await safeFetch(url, { headers, signal: signal as any });
|
const resp = await safeFetch(url, { headers, signal: signal as any });
|
||||||
|
|
||||||
response.status(resp.status);
|
response.status(resp.status);
|
||||||
if (resp.headers.get('content-type')) response.set('Content-Type', resp.headers.get('content-type') as string);
|
if (resp.headers.get('content-type')) response.set('Content-Type', resp.headers.get('content-type') as string);
|
||||||
if (resp.headers.get('cache-control')) response.set('Cache-Control', resp.headers.get('cache-control') as string);
|
if (!resp.ok) {
|
||||||
|
response.set('Cache-Control', 'no-store, max-age=0');
|
||||||
|
} else if (resp.headers.get('cache-control')) {
|
||||||
|
response.set('Cache-Control', resp.headers.get('cache-control') as string);
|
||||||
|
} else if (defaultCacheControl) {
|
||||||
|
response.set('Cache-Control', defaultCacheControl);
|
||||||
|
}
|
||||||
if (resp.headers.get('content-length')) response.set('Content-Length', resp.headers.get('content-length') as string);
|
if (resp.headers.get('content-length')) response.set('Content-Length', resp.headers.get('content-length') as string);
|
||||||
if (resp.headers.get('content-disposition')) response.set('Content-Disposition', resp.headers.get('content-disposition') as string);
|
if (resp.headers.get('content-disposition')) response.set('Content-Disposition', resp.headers.get('content-disposition') as string);
|
||||||
|
|
||||||
|
|||||||
@@ -246,8 +246,7 @@ export async function streamImmichAsset(
|
|||||||
? `${creds.immich_url}/api/assets/${assetId}/thumbnail?size=thumbnail`
|
? `${creds.immich_url}/api/assets/${assetId}/thumbnail?size=thumbnail`
|
||||||
: `${creds.immich_url}/api/assets/${assetId}/thumbnail?size=fullsize`;
|
: `${creds.immich_url}/api/assets/${assetId}/thumbnail?size=fullsize`;
|
||||||
|
|
||||||
response.set('Cache-Control', 'public, max-age=86400');
|
await pipeAsset(url, response, { 'x-api-key': creds.immich_api_key }, AbortSignal.timeout(timeout), 'public, max-age=86400');
|
||||||
await pipeAsset(url, response, { 'x-api-key': creds.immich_api_key }, AbortSignal.timeout(timeout));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Albums ──────────────────────────────────────────────────────────────────
|
// ── Albums ──────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -69,15 +69,18 @@ export async function streamPhoto(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (photo.file_path) {
|
||||||
|
const localPath = path.join(__dirname, '../../../uploads', photo.file_path);
|
||||||
|
if (fs.existsSync(localPath)) {
|
||||||
|
res.set('Cache-Control', 'public, max-age=86400');
|
||||||
|
res.sendFile(localPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (photo.provider) {
|
switch (photo.provider) {
|
||||||
case 'local': {
|
case 'local': {
|
||||||
const filePath = path.join(__dirname, '../../../uploads', photo.file_path!);
|
res.status(404).json({ error: 'File not found' });
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
res.status(404).json({ error: 'File not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
res.set('Cache-Control', 'public, max-age=86400');
|
|
||||||
res.sendFile(filePath);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case 'immich': {
|
case 'immich': {
|
||||||
|
|||||||
@@ -661,6 +661,6 @@ export async function streamSynologyAsset(
|
|||||||
if (passphrase) params.append('passphrase', passphrase);
|
if (passphrase) params.append('passphrase', passphrase);
|
||||||
|
|
||||||
const url = _buildSynologyEndpoint(synology_credentials.data.synology_url, params.toString());
|
const url = _buildSynologyEndpoint(synology_credentials.data.synology_url, params.toString());
|
||||||
await pipeAsset(url, response)
|
await pipeAsset(url, response, undefined, undefined, 'public, max-age=86400')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user