This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { listHetznerKeys, uploadToHetznerWithKey } from "@/lib/storage";
|
||||
|
||||
export const maxDuration = 300; // 5 min — large video uploads
|
||||
|
||||
const MIME_MAP: Record<string, string> = {
|
||||
".mp4": "video/mp4",
|
||||
".mov": "video/quicktime",
|
||||
".avi": "video/x-msvideo",
|
||||
".mxf": "application/mxf",
|
||||
".webm": "video/webm",
|
||||
".jpg": "image/jpeg",
|
||||
".jpeg": "image/jpeg",
|
||||
".png": "image/png",
|
||||
".gif": "image/gif",
|
||||
".webp": "image/webp",
|
||||
};
|
||||
|
||||
function walkDir(
|
||||
dir: string,
|
||||
base: string
|
||||
): Array<{ key: string; sizeBytes: number }> {
|
||||
const results: Array<{ key: string; sizeBytes: number }> = [];
|
||||
if (!fs.existsSync(dir)) return results;
|
||||
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const abs = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...walkDir(abs, base));
|
||||
} else if (entry.isFile()) {
|
||||
const key = path.relative(base, abs).replace(/\\/g, "/");
|
||||
results.push({ key, sizeBytes: fs.statSync(abs).size });
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** GET /api/admin/migration — list local files with Hetzner sync status */
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const uploadDir = path.resolve(process.env.LOCAL_UPLOAD_DIR ?? "./uploads");
|
||||
const localFiles = walkDir(uploadDir, uploadDir);
|
||||
|
||||
let hetznerKeys = new Set<string>();
|
||||
let hetznerConfigured = true;
|
||||
try {
|
||||
hetznerKeys = await listHetznerKeys();
|
||||
} catch {
|
||||
hetznerConfigured = false;
|
||||
}
|
||||
|
||||
const files = localFiles.map(({ key, sizeBytes }) => ({
|
||||
key,
|
||||
filename: path.basename(key),
|
||||
folder: path.dirname(key).replace(/\\/g, "/") || "root",
|
||||
sizeBytes,
|
||||
hetznerExists: hetznerKeys.has(key),
|
||||
}));
|
||||
|
||||
return NextResponse.json({ files, hetznerConfigured });
|
||||
}
|
||||
|
||||
/** POST /api/admin/migration — migrate a single file to Hetzner */
|
||||
export async function POST(req: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
const key = body?.key;
|
||||
if (!key || typeof key !== "string") {
|
||||
return NextResponse.json({ error: "key is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const uploadDir = path.resolve(process.env.LOCAL_UPLOAD_DIR ?? "./uploads");
|
||||
const absPath = path.resolve(path.join(uploadDir, key));
|
||||
|
||||
// Path traversal guard
|
||||
if (!absPath.startsWith(uploadDir + path.sep)) {
|
||||
return NextResponse.json({ error: "Invalid key" }, { status: 403 });
|
||||
}
|
||||
|
||||
let stat: fs.Stats;
|
||||
try {
|
||||
stat = fs.statSync(absPath);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "File not found on disk" }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!stat.isFile()) {
|
||||
return NextResponse.json({ error: "Not a file" }, { status: 400 });
|
||||
}
|
||||
|
||||
const ext = path.extname(key).toLowerCase();
|
||||
const contentType = MIME_MAP[ext] ?? "application/octet-stream";
|
||||
|
||||
const start = Date.now();
|
||||
try {
|
||||
const stream = fs.createReadStream(absPath);
|
||||
await uploadToHetznerWithKey(stream, key, contentType, stat.size);
|
||||
return NextResponse.json({ success: true, key, ms: Date.now() - start });
|
||||
} catch (e: unknown) {
|
||||
return NextResponse.json(
|
||||
{ error: e instanceof Error ? e.message : "Upload failed", key },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { uploadFile, uploadToHetzner, deleteFromHetzner } from "@/lib/storage";
|
||||
import { uploadToHetzner, deleteFromHetzner } from "@/lib/storage";
|
||||
import { recalcShotStatus } from "@/lib/shot-status";
|
||||
|
||||
export const maxDuration = 120;
|
||||
@@ -133,8 +133,9 @@ export async function POST(
|
||||
);
|
||||
}
|
||||
|
||||
// Upload the video to the configured storage backend
|
||||
const result = await uploadFile(buffer, file.name, file.type, "videos");
|
||||
// Upload the video to Hetzner object storage
|
||||
const { key: videoKey } = await uploadToHetzner(buffer, file.name, file.type, "videos");
|
||||
const result = { url: `/api/files/${videoKey}`, key: videoKey };
|
||||
|
||||
// Mark all existing versions for this task as no longer latest
|
||||
await db.version.updateMany({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/lib/db";
|
||||
import { uploadFile } from "@/lib/storage";
|
||||
import { uploadToHetzner } from "@/lib/storage";
|
||||
import { ShotPriority } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -169,8 +169,8 @@ export async function POST(req: NextRequest) {
|
||||
);
|
||||
}
|
||||
const buffer = Buffer.from(await thumbnailFile.arrayBuffer());
|
||||
const result = await uploadFile(buffer, thumbnailFile.name, thumbnailFile.type, "image");
|
||||
thumbnailUrl = result.url;
|
||||
const { key: thumbKey } = await uploadToHetzner(buffer, thumbnailFile.name, thumbnailFile.type, "image");
|
||||
thumbnailUrl = `/api/files/${thumbKey}`;
|
||||
}
|
||||
|
||||
// Resolve shot group
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { generateHetznerStreamUrl } from "@/lib/storage";
|
||||
|
||||
// ── Tuning constants ──────────────────────────────────────────────────────────
|
||||
//
|
||||
@@ -50,7 +51,15 @@ export async function GET(
|
||||
try {
|
||||
stat = fs.statSync(filePath);
|
||||
} catch {
|
||||
return new NextResponse("Not found", { status: 404 });
|
||||
// File not on local disk — attempt a redirect to Hetzner object storage.
|
||||
// This covers files uploaded after the migration and any migrated files
|
||||
// whose local copies have been removed.
|
||||
try {
|
||||
const hetznerUrl = await generateHetznerStreamUrl(relativePath, 3600);
|
||||
return NextResponse.redirect(hetznerUrl, 302);
|
||||
} catch {
|
||||
return new NextResponse("Not found", { status: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
// Guard against directory traversal that resolves to a directory
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { uploadFile } from "@/lib/storage";
|
||||
import { uploadToHetzner } from "@/lib/storage";
|
||||
|
||||
function requireAuth(role: string) {
|
||||
return ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(role);
|
||||
@@ -58,7 +58,8 @@ export async function POST(
|
||||
return NextResponse.json({ error: "File too large (max 50 MB)" }, { status: 413 });
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const uploaded = await uploadFile(buffer, file.name, file.type, "image");
|
||||
const { key: refKey } = await uploadToHetzner(buffer, file.name, file.type, "image");
|
||||
const uploaded = { url: `/api/files/${refKey}`, key: refKey };
|
||||
|
||||
const maxOrder = await db.shotReference.aggregate({
|
||||
where: { shotId },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { db } from "@/lib/db";
|
||||
import { uploadFile } from "@/lib/storage";
|
||||
import { uploadToHetzner } from "@/lib/storage";
|
||||
|
||||
function canManage(role: string) {
|
||||
return ["ADMIN", "PRODUCER", "SUPERVISOR"].includes(role);
|
||||
@@ -44,7 +44,8 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: "File too large (max 20 MB)" }, { status: 413 });
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const uploaded = await uploadFile(buffer, file.name, file.type, "image");
|
||||
const { key: tmplKey } = await uploadToHetzner(buffer, file.name, file.type, "image");
|
||||
const uploaded = { url: `/api/files/${tmplKey}`, key: tmplKey };
|
||||
|
||||
const maxOrder = await db.sketchTemplate.aggregate({ _max: { sortOrder: true } });
|
||||
const sortOrder = (maxOrder._max.sortOrder ?? -1) + 1;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { uploadFile } from "@/lib/storage";
|
||||
import { uploadToHetzner } from "@/lib/storage";
|
||||
|
||||
export const config = { api: { bodyParser: false } };
|
||||
|
||||
@@ -30,9 +30,9 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const result = await uploadFile(buffer, file.name, file.type, "videos");
|
||||
const { key } = await uploadToHetzner(buffer, file.name, file.type, "videos");
|
||||
|
||||
return NextResponse.json({ url: result.url, key: result.key });
|
||||
return NextResponse.json({ url: `/api/files/${key}`, key });
|
||||
} catch (err) {
|
||||
console.error("[local-upload]", err);
|
||||
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/auth";
|
||||
import { uploadFile } from "@/lib/storage";
|
||||
import { uploadToHetzner } from "@/lib/storage";
|
||||
|
||||
export const config = { api: { bodyParser: false } };
|
||||
|
||||
@@ -41,9 +41,9 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
const result = await uploadFile(buffer, file.name, file.type, type);
|
||||
const { key } = await uploadToHetzner(buffer, file.name, file.type, type);
|
||||
|
||||
return NextResponse.json({ url: result.url, key: result.key });
|
||||
return NextResponse.json({ url: `/api/files/${key}`, key });
|
||||
} catch (err) {
|
||||
console.error("[upload]", err);
|
||||
return NextResponse.json({ error: "Upload failed" }, { status: 500 });
|
||||
|
||||
Reference in New Issue
Block a user