fix: media serving and id consistency

- server.ts: Replace sendFile with manual readFile+send to fix
  NotFoundError when serving media (sendFile failed even after stat)
- store.ts: Return id with file extension so it matches actual filename

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joao Lisboa
2025-12-02 13:21:16 -03:00
committed by Peter Steinberger
parent 2ec9192010
commit 2fae0a9f47
2 changed files with 17 additions and 13 deletions

View File

@@ -4,6 +4,7 @@ import path from "node:path";
import express, { type Express } from "express"; import express, { type Express } from "express";
import { danger } from "../globals.js"; import { danger } from "../globals.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { detectMime } from "./mime.js";
import { cleanOldMedia, getMediaDir } from "./store.js"; import { cleanOldMedia, getMediaDir } from "./store.js";
const DEFAULT_TTL_MS = 2 * 60 * 1000; const DEFAULT_TTL_MS = 2 * 60 * 1000;
@@ -19,7 +20,6 @@ export function attachMediaRoutes(
const id = req.params.id; const id = req.params.id;
const mediaRoot = (await fs.realpath(mediaDir)) + path.sep; const mediaRoot = (await fs.realpath(mediaDir)) + path.sep;
const file = path.resolve(mediaRoot, id); const file = path.resolve(mediaRoot, id);
try { try {
const lstat = await fs.lstat(file); const lstat = await fs.lstat(file);
if (lstat.isSymbolicLink()) { if (lstat.isSymbolicLink()) {
@@ -37,13 +37,14 @@ export function attachMediaRoutes(
res.status(410).send("expired"); res.status(410).send("expired");
return; return;
} }
res.sendFile(realPath); const data = await fs.readFile(realPath);
const mime = detectMime({ buffer: data, filePath: realPath });
if (mime) res.type(mime);
res.send(data);
// best-effort single-use cleanup after response ends // best-effort single-use cleanup after response ends
res.on("finish", () => { setTimeout(() => {
setTimeout(() => { fs.rm(realPath).catch(() => {});
fs.rm(realPath).catch(() => {}); }, 500);
}, 500);
});
} catch { } catch {
res.status(404).send("not found"); res.status(404).send("not found");
} }

View File

@@ -107,9 +107,9 @@ export async function saveMediaSource(
const dir = subdir ? path.join(MEDIA_DIR, subdir) : MEDIA_DIR; const dir = subdir ? path.join(MEDIA_DIR, subdir) : MEDIA_DIR;
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
await cleanOldMedia(); await cleanOldMedia();
const id = crypto.randomUUID(); const baseId = crypto.randomUUID();
if (looksLikeUrl(source)) { if (looksLikeUrl(source)) {
const tempDest = path.join(dir, `${id}.tmp`); const tempDest = path.join(dir, `${baseId}.tmp`);
const { headerMime, sniffBuffer, size } = await downloadToFile( const { headerMime, sniffBuffer, size } = await downloadToFile(
source, source,
tempDest, tempDest,
@@ -122,7 +122,8 @@ export async function saveMediaSource(
}); });
const ext = const ext =
extensionForMime(mime) ?? path.extname(new URL(source).pathname); extensionForMime(mime) ?? path.extname(new URL(source).pathname);
const finalDest = path.join(dir, ext ? `${id}${ext}` : id); const id = ext ? `${baseId}${ext}` : baseId;
const finalDest = path.join(dir, id);
await fs.rename(tempDest, finalDest); await fs.rename(tempDest, finalDest);
return { id, path: finalDest, size, contentType: mime }; return { id, path: finalDest, size, contentType: mime };
} }
@@ -137,7 +138,8 @@ export async function saveMediaSource(
const buffer = await fs.readFile(source); const buffer = await fs.readFile(source);
const mime = detectMime({ buffer, filePath: source }); const mime = detectMime({ buffer, filePath: source });
const ext = extensionForMime(mime) ?? path.extname(source); const ext = extensionForMime(mime) ?? path.extname(source);
const dest = path.join(dir, ext ? `${id}${ext}` : id); const id = ext ? `${baseId}${ext}` : baseId;
const dest = path.join(dir, id);
await fs.writeFile(dest, buffer); await fs.writeFile(dest, buffer);
return { id, path: dest, size: stat.size, contentType: mime }; return { id, path: dest, size: stat.size, contentType: mime };
} }
@@ -152,10 +154,11 @@ export async function saveMediaBuffer(
} }
const dir = path.join(MEDIA_DIR, subdir); const dir = path.join(MEDIA_DIR, subdir);
await fs.mkdir(dir, { recursive: true }); await fs.mkdir(dir, { recursive: true });
const id = crypto.randomUUID(); const baseId = crypto.randomUUID();
const mime = detectMime({ buffer, headerMime: contentType }); const mime = detectMime({ buffer, headerMime: contentType });
const ext = extensionForMime(mime); const ext = extensionForMime(mime);
const dest = path.join(dir, ext ? `${id}${ext}` : id); const id = ext ? `${baseId}${ext}` : baseId;
const dest = path.join(dir, id);
await fs.writeFile(dest, buffer); await fs.writeFile(dest, buffer);
return { id, path: dest, size: buffer.byteLength, contentType: mime }; return { id, path: dest, size: buffer.byteLength, contentType: mime };
} }