This commit is contained in:
commit
59bb5a38a3
132 files changed
+19435
No files matched your search
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Auslieferung freigegebener Medien aus dem persistenten Verzeichnis
|
||||
* MEDIA_DIR (Kubernetes: eigenes PersistentVolume, kein emptyDir).
|
||||
*
|
||||
* Nur lesend, nur erlaubte Dateiendungen, kein Verlassen des Verzeichnisses.
|
||||
* Unterstuetzt Range-Anfragen, damit Videos spulbar sind.
|
||||
*/
|
||||
import { createReadStream } from "node:fs";
|
||||
import { stat } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { Readable } from "node:stream";
|
||||
import { env } from "@/lib/env";
|
||||
|
||||
const TYPES: Record<string, string> = {
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".webp": "image/webp",
|
||||
".avif": "image/avif",
|
||||
".mp4": "video/mp4",
|
||||
".webm": "video/webm",
|
||||
".vtt": "text/vtt",
|
||||
};
|
||||
|
||||
const SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,120}$/;
|
||||
|
||||
export async function GET(request: Request, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
const { path: segments } = await params;
|
||||
const notFound = new Response("Nicht gefunden", { status: 404 });
|
||||
if (segments.length === 0 || segments.length > 4 || !segments.every((s) => SEGMENT.test(s) && !s.includes(".."))) return notFound;
|
||||
|
||||
const base = path.resolve(env().MEDIA_DIR);
|
||||
const file = path.resolve(base, ...segments);
|
||||
if (!file.startsWith(base + path.sep)) return notFound;
|
||||
const type = TYPES[path.extname(file).toLowerCase()];
|
||||
if (!type) return notFound;
|
||||
|
||||
let info;
|
||||
try {
|
||||
info = await stat(file);
|
||||
} catch {
|
||||
return notFound;
|
||||
}
|
||||
if (!info.isFile()) return notFound;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": type,
|
||||
"Accept-Ranges": "bytes",
|
||||
"Cache-Control": "public, max-age=86400",
|
||||
"X-Content-Type-Options": "nosniff",
|
||||
};
|
||||
|
||||
const range = /^bytes=(\d*)-(\d*)$/.exec(request.headers.get("range") ?? "");
|
||||
if (range && (range[1] || range[2])) {
|
||||
const size = info.size;
|
||||
let start = range[1] ? Number(range[1]) : size - Number(range[2]);
|
||||
let end = range[1] && range[2] ? Number(range[2]) : size - 1;
|
||||
start = Math.max(0, start);
|
||||
end = Math.min(end, size - 1);
|
||||
if (start > end) return new Response(null, { status: 416, headers: { "Content-Range": `bytes */${size}` } });
|
||||
const stream = Readable.toWeb(createReadStream(file, { start, end })) as ReadableStream;
|
||||
return new Response(stream, {
|
||||
status: 206,
|
||||
headers: { ...headers, "Content-Range": `bytes ${start}-${end}/${size}`, "Content-Length": String(end - start + 1) },
|
||||
});
|
||||
}
|
||||
|
||||
const stream = Readable.toWeb(createReadStream(file)) as ReadableStream;
|
||||
return new Response(stream, { headers: { ...headers, "Content-Length": String(info.size) } });
|
||||
}
|
||||
Reference in new issue
Block a user