165 lines
5.3 KiB
TypeScript
165 lines
5.3 KiB
TypeScript
import { Router, Request, Response } from "express";
|
|
import { storagePut } from "./storage";
|
|
import { nanoid } from "nanoid";
|
|
|
|
const uploadRouter = Router();
|
|
|
|
// Video upload endpoint
|
|
uploadRouter.post("/api/upload/video", async (req: Request, res: Response) => {
|
|
try {
|
|
// Handle multipart form data
|
|
const chunks: Buffer[] = [];
|
|
|
|
req.on("data", (chunk: Buffer) => {
|
|
chunks.push(chunk);
|
|
});
|
|
|
|
req.on("end", async () => {
|
|
try {
|
|
const body = Buffer.concat(chunks);
|
|
|
|
// Parse multipart boundary
|
|
const contentType = req.headers["content-type"] || "";
|
|
const boundary = contentType.split("boundary=")[1];
|
|
|
|
if (!boundary) {
|
|
res.status(400).json({ error: "Invalid content type" });
|
|
return;
|
|
}
|
|
|
|
// Simple multipart parsing - extract file data
|
|
const bodyStr = body.toString("binary");
|
|
const parts = bodyStr.split(`--${boundary}`);
|
|
|
|
let fileBuffer: Buffer | null = null;
|
|
let fileName = "video.mp4";
|
|
let projectName = "";
|
|
|
|
for (const part of parts) {
|
|
if (part.includes('name="file"')) {
|
|
const headerEnd = part.indexOf("\r\n\r\n");
|
|
if (headerEnd !== -1) {
|
|
// Extract filename
|
|
const filenameMatch = part.match(/filename="([^"]+)"/);
|
|
if (filenameMatch) fileName = filenameMatch[1];
|
|
|
|
// Extract file content
|
|
const content = part.substring(headerEnd + 4);
|
|
// Remove trailing \r\n
|
|
const trimmed = content.endsWith("\r\n") ? content.slice(0, -2) : content;
|
|
fileBuffer = Buffer.from(trimmed, "binary");
|
|
}
|
|
} else if (part.includes('name="name"')) {
|
|
const headerEnd = part.indexOf("\r\n\r\n");
|
|
if (headerEnd !== -1) {
|
|
projectName = part.substring(headerEnd + 4).trim().replace(/\r\n$/, "");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fileBuffer) {
|
|
res.status(400).json({ error: "No file provided" });
|
|
return;
|
|
}
|
|
|
|
// Upload to S3 storage
|
|
const fileId = nanoid(12);
|
|
const ext = fileName.split(".").pop() || "mp4";
|
|
const storageKey = `videos/${fileId}.${ext}`;
|
|
|
|
const { url } = await storagePut(storageKey, fileBuffer, `video/${ext}`);
|
|
|
|
res.json({
|
|
success: true,
|
|
url,
|
|
fileName,
|
|
fileId,
|
|
size: fileBuffer.length,
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Upload] Processing error:", err);
|
|
res.status(500).json({ error: "Upload processing failed" });
|
|
}
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Upload] Error:", err);
|
|
res.status(500).json({ error: "Upload failed" });
|
|
}
|
|
});
|
|
|
|
// Image/asset upload endpoint (for reference sheets, etc.)
|
|
uploadRouter.post("/api/upload/asset", async (req: Request, res: Response) => {
|
|
try {
|
|
const chunks: Buffer[] = [];
|
|
|
|
req.on("data", (chunk: Buffer) => {
|
|
chunks.push(chunk);
|
|
});
|
|
|
|
req.on("end", async () => {
|
|
try {
|
|
const body = Buffer.concat(chunks);
|
|
const contentType = req.headers["content-type"] || "";
|
|
const boundary = contentType.split("boundary=")[1];
|
|
|
|
if (!boundary) {
|
|
res.status(400).json({ error: "Invalid content type" });
|
|
return;
|
|
}
|
|
|
|
const bodyStr = body.toString("binary");
|
|
const parts = bodyStr.split(`--${boundary}`);
|
|
|
|
let fileBuffer: Buffer | null = null;
|
|
let fileName = "asset.png";
|
|
let assetType = "reference";
|
|
|
|
for (const part of parts) {
|
|
if (part.includes('name="file"')) {
|
|
const headerEnd = part.indexOf("\r\n\r\n");
|
|
if (headerEnd !== -1) {
|
|
const filenameMatch = part.match(/filename="([^"]+)"/);
|
|
if (filenameMatch) fileName = filenameMatch[1];
|
|
const content = part.substring(headerEnd + 4);
|
|
const trimmed = content.endsWith("\r\n") ? content.slice(0, -2) : content;
|
|
fileBuffer = Buffer.from(trimmed, "binary");
|
|
}
|
|
} else if (part.includes('name="type"')) {
|
|
const headerEnd = part.indexOf("\r\n\r\n");
|
|
if (headerEnd !== -1) {
|
|
assetType = part.substring(headerEnd + 4).trim().replace(/\r\n$/, "");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!fileBuffer) {
|
|
res.status(400).json({ error: "No file provided" });
|
|
return;
|
|
}
|
|
|
|
const fileId = nanoid(12);
|
|
const ext = fileName.split(".").pop() || "png";
|
|
const storageKey = `assets/${assetType}/${fileId}.${ext}`;
|
|
const mimeType = ext === "png" ? "image/png" : ext === "jpg" || ext === "jpeg" ? "image/jpeg" : `image/${ext}`;
|
|
|
|
const { url } = await storagePut(storageKey, fileBuffer, mimeType);
|
|
|
|
res.json({
|
|
success: true,
|
|
url,
|
|
fileName,
|
|
fileId,
|
|
size: fileBuffer.length,
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Upload Asset] Processing error:", err);
|
|
res.status(500).json({ error: "Asset upload processing failed" });
|
|
}
|
|
});
|
|
} catch (err: any) {
|
|
console.error("[Upload Asset] Error:", err);
|
|
res.status(500).json({ error: "Asset upload failed" });
|
|
}
|
|
});
|
|
|
|
export default uploadRouter;
|