73 lines
2.3 KiB
TypeScript
73 lines
2.3 KiB
TypeScript
// app/api/transcoder/download/[videoId]/route.ts
|
||
// Streams the original MP4 directly to the remote transcoder worker.
|
||
// Never buffers the file in memory.
|
||
|
||
import { NextResponse } from "next/server";
|
||
import * as fssync from "fs";
|
||
import * as fs from "fs/promises";
|
||
import * as path from "path";
|
||
import { Readable } from "stream";
|
||
import { prisma } from "@/lib/prisma";
|
||
import { verifyTranscoderToken } from "@/lib/transcoder-auth";
|
||
|
||
// Allow up to 45 minutes for large file transfers.
|
||
export const maxDuration = 2700;
|
||
|
||
const UPLOADS_DIR = process.env.UPLOADS_DIR ?? "/uploads";
|
||
const ORIGINALS_DIR = path.join(UPLOADS_DIR, "videos");
|
||
|
||
// Narrow character set – CUIDs are alphanumeric plus underscore/dash.
|
||
const SAFE_ID = /^[a-zA-Z0-9_-]{1,64}$/;
|
||
|
||
export async function GET(
|
||
request: Request,
|
||
context: { params: Promise<{ videoId: string }> }
|
||
) {
|
||
if (!verifyTranscoderToken(request)) {
|
||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||
}
|
||
|
||
const { videoId } = await context.params;
|
||
|
||
if (!SAFE_ID.test(videoId)) {
|
||
return NextResponse.json({ error: "Invalid video ID" }, { status: 400 });
|
||
}
|
||
|
||
const video = await prisma.video.findUnique({ where: { id: videoId } });
|
||
if (!video) {
|
||
return NextResponse.json({ error: "Not found" }, { status: 404 });
|
||
}
|
||
|
||
// Only allow download while the job is actively claimed.
|
||
if (video.transcodingStatus !== "processing") {
|
||
return NextResponse.json(
|
||
{ error: "Video is not in processing state" },
|
||
{ status: 409 }
|
||
);
|
||
}
|
||
|
||
const filePath = path.join(ORIGINALS_DIR, `${videoId}.mp4`);
|
||
|
||
// Security: ensure the resolved path stays within ORIGINALS_DIR.
|
||
const resolved = path.resolve(filePath);
|
||
if (!resolved.startsWith(path.resolve(ORIGINALS_DIR))) {
|
||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||
}
|
||
|
||
try {
|
||
const stat = await fs.stat(resolved);
|
||
const nodeStream = fssync.createReadStream(resolved);
|
||
const webStream = Readable.toWeb(nodeStream) as ReadableStream;
|
||
|
||
return new Response(webStream, {
|
||
headers: {
|
||
"Content-Type": "video/mp4",
|
||
"Content-Length": stat.size.toString(),
|
||
"Content-Disposition": `attachment; filename="${videoId}.mp4"`,
|
||
},
|
||
});
|
||
} catch {
|
||
return NextResponse.json({ error: "File not found on disk" }, { status: 404 });
|
||
}
|
||
}
|