From 2fae0a9f47a072a51212b280519db28b243dec0c Mon Sep 17 00:00:00 2001 From: Joao Lisboa Date: Tue, 2 Dec 2025 13:21:16 -0300 Subject: [PATCH] fix: media serving and id consistency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- src/media/server.ts | 15 ++++++++------- src/media/store.ts | 15 +++++++++------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/media/server.ts b/src/media/server.ts index 1c37c2a33..ad830da32 100644 --- a/src/media/server.ts +++ b/src/media/server.ts @@ -4,6 +4,7 @@ import path from "node:path"; import express, { type Express } from "express"; import { danger } from "../globals.js"; import { defaultRuntime, type RuntimeEnv } from "../runtime.js"; +import { detectMime } from "./mime.js"; import { cleanOldMedia, getMediaDir } from "./store.js"; const DEFAULT_TTL_MS = 2 * 60 * 1000; @@ -19,7 +20,6 @@ export function attachMediaRoutes( const id = req.params.id; const mediaRoot = (await fs.realpath(mediaDir)) + path.sep; const file = path.resolve(mediaRoot, id); - try { const lstat = await fs.lstat(file); if (lstat.isSymbolicLink()) { @@ -37,13 +37,14 @@ export function attachMediaRoutes( res.status(410).send("expired"); 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 - res.on("finish", () => { - setTimeout(() => { - fs.rm(realPath).catch(() => {}); - }, 500); - }); + setTimeout(() => { + fs.rm(realPath).catch(() => {}); + }, 500); } catch { res.status(404).send("not found"); } diff --git a/src/media/store.ts b/src/media/store.ts index 291d048e8..82bb4c8c5 100644 --- a/src/media/store.ts +++ b/src/media/store.ts @@ -107,9 +107,9 @@ export async function saveMediaSource( const dir = subdir ? path.join(MEDIA_DIR, subdir) : MEDIA_DIR; await fs.mkdir(dir, { recursive: true }); await cleanOldMedia(); - const id = crypto.randomUUID(); + const baseId = crypto.randomUUID(); if (looksLikeUrl(source)) { - const tempDest = path.join(dir, `${id}.tmp`); + const tempDest = path.join(dir, `${baseId}.tmp`); const { headerMime, sniffBuffer, size } = await downloadToFile( source, tempDest, @@ -122,7 +122,8 @@ export async function saveMediaSource( }); const ext = 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); return { id, path: finalDest, size, contentType: mime }; } @@ -137,7 +138,8 @@ export async function saveMediaSource( const buffer = await fs.readFile(source); const mime = detectMime({ buffer, filePath: 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); return { id, path: dest, size: stat.size, contentType: mime }; } @@ -152,10 +154,11 @@ export async function saveMediaBuffer( } const dir = path.join(MEDIA_DIR, subdir); await fs.mkdir(dir, { recursive: true }); - const id = crypto.randomUUID(); + const baseId = crypto.randomUUID(); const mime = detectMime({ buffer, headerMime: contentType }); 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); return { id, path: dest, size: buffer.byteLength, contentType: mime }; }