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:
committed by
Peter Steinberger
parent
2ec9192010
commit
2fae0a9f47
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user