68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import fs from "fs";
|
|
import path from "path";
|
|
|
|
export async function GET(
|
|
req: NextRequest,
|
|
{ params }: { params: Promise<{ key: string[] }> }
|
|
) {
|
|
const { key } = await params;
|
|
// key is a catch-all segment array, e.g. ["videos", "uuid-filename.mp4"]
|
|
const relativePath = key.join("/");
|
|
|
|
// Sanitize: prevent path traversal
|
|
const uploadDir = path.resolve(process.env.LOCAL_UPLOAD_DIR ?? "./uploads");
|
|
const filePath = path.resolve(path.join(uploadDir, relativePath));
|
|
|
|
if (!filePath.startsWith(uploadDir)) {
|
|
return new NextResponse("Forbidden", { status: 403 });
|
|
}
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return new NextResponse("Not found", { status: 404 });
|
|
}
|
|
|
|
const stat = fs.statSync(filePath);
|
|
const ext = path.extname(filePath).toLowerCase();
|
|
const mimeMap: Record<string, string> = {
|
|
".mp4": "video/mp4",
|
|
".mov": "video/quicktime",
|
|
".avi": "video/x-msvideo",
|
|
".mxf": "application/mxf",
|
|
".webm": "video/webm",
|
|
};
|
|
const contentType = mimeMap[ext] ?? "application/octet-stream";
|
|
|
|
// Support range requests so the HTML5 video player can seek
|
|
const rangeHeader = req.headers.get("range");
|
|
|
|
if (rangeHeader) {
|
|
const [startStr, endStr] = rangeHeader.replace("bytes=", "").split("-");
|
|
const start = parseInt(startStr, 10);
|
|
const end = endStr ? parseInt(endStr, 10) : stat.size - 1;
|
|
const chunkSize = end - start + 1;
|
|
|
|
const stream = fs.createReadStream(filePath, { start, end });
|
|
const nodeStream = stream as unknown as ReadableStream;
|
|
|
|
return new NextResponse(nodeStream, {
|
|
status: 206,
|
|
headers: {
|
|
"Content-Range": `bytes ${start}-${end}/${stat.size}`,
|
|
"Accept-Ranges": "bytes",
|
|
"Content-Length": String(chunkSize),
|
|
"Content-Type": contentType,
|
|
},
|
|
});
|
|
}
|
|
|
|
const stream = fs.createReadStream(filePath) as unknown as ReadableStream;
|
|
return new NextResponse(stream, {
|
|
headers: {
|
|
"Content-Length": String(stat.size),
|
|
"Content-Type": contentType,
|
|
"Accept-Ranges": "bytes",
|
|
},
|
|
});
|
|
}
|