98 lines
2.6 KiB
TypeScript
98 lines
2.6 KiB
TypeScript
/**
|
|
* POST endpoint to upload an image file to the OS temp directory.
|
|
*
|
|
* POST /api/terminal/upload
|
|
* Body: multipart/form-data with a single `file` field
|
|
*
|
|
* Returns:
|
|
* 200 { ok: true, path: "/tmp/sf-upload-..." }
|
|
* 400 { error: "No file provided" }
|
|
* 413 { error: "File too large (...)" }
|
|
* 415 { error: "Unsupported image type: ..." }
|
|
* 500 { error: "Failed to write file: ..." }
|
|
*
|
|
* Observability:
|
|
* - Structured error responses with descriptive messages
|
|
* - No custom cleanup — OS handles temp dir cleanup on reboot
|
|
*/
|
|
|
|
import { randomBytes } from "node:crypto";
|
|
import { writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
export const runtime = "nodejs";
|
|
export const dynamic = "force-dynamic";
|
|
|
|
const ALLOWED_MIME_TYPES = new Set([
|
|
"image/jpeg",
|
|
"image/png",
|
|
"image/gif",
|
|
"image/webp",
|
|
]);
|
|
|
|
const MIME_TO_EXT: Record<string, string> = {
|
|
"image/jpeg": "jpg",
|
|
"image/png": "png",
|
|
"image/gif": "gif",
|
|
"image/webp": "webp",
|
|
};
|
|
|
|
/** 20 MB raw file size limit */
|
|
const MAX_FILE_SIZE = 20 * 1024 * 1024;
|
|
|
|
export async function POST(request: Request): Promise<Response> {
|
|
let formData: FormData;
|
|
try {
|
|
formData = await request.formData();
|
|
} catch {
|
|
return Response.json(
|
|
{ error: "Invalid multipart form data" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const file = formData.get("file");
|
|
if (!file || !(file instanceof File)) {
|
|
return Response.json({ error: "No file provided" }, { status: 400 });
|
|
}
|
|
|
|
// Validate MIME type
|
|
if (!ALLOWED_MIME_TYPES.has(file.type)) {
|
|
return Response.json(
|
|
{
|
|
error: `Unsupported image type: ${file.type || "unknown"}. Accepted: JPEG, PNG, GIF, WebP.`,
|
|
},
|
|
{ status: 415 },
|
|
);
|
|
}
|
|
|
|
// Validate file size
|
|
if (file.size > MAX_FILE_SIZE) {
|
|
const sizeMB = (file.size / (1024 * 1024)).toFixed(1);
|
|
return Response.json(
|
|
{ error: `File too large (${sizeMB} MB). Maximum: 20 MB.` },
|
|
{ status: 413 },
|
|
);
|
|
}
|
|
|
|
// Generate unique filename and write to temp dir
|
|
const ext = MIME_TO_EXT[file.type] ?? "bin";
|
|
const hex = randomBytes(4).toString("hex");
|
|
const filename = `sf-upload-${Date.now()}-${hex}.${ext}`;
|
|
const filePath = join(tmpdir(), filename);
|
|
|
|
try {
|
|
const arrayBuffer = await file.arrayBuffer();
|
|
await writeFile(filePath, Buffer.from(arrayBuffer));
|
|
} catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
console.error("[terminal-upload] Failed to write file:", message);
|
|
return Response.json(
|
|
{ error: `Failed to write file: ${message}` },
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
|
|
return Response.json({ ok: true, path: filePath });
|
|
}
|