import { randomUUID } from "crypto"; import { mkdir, unlink, writeFile } from "fs/promises"; import path from "path"; const allowedExtensions = new Set([ ".png", ".jpg", ".jpeg", ".webp", ".svg", ".pdf", ".docx", ".doc", ".xlsx", ".xls", ".pptx", ".zip", ".mp4", ".mov" ]); const allowedMimes = new Set([ "image/png", "image/jpeg", "image/webp", "image/svg+xml", "application/pdf", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/msword", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/zip", "video/mp4", "video/quicktime" ]); export function getUploadDir() { return process.env.UPLOAD_DIR ?? path.join(process.cwd(), "storage", "uploads"); } export function getUploadMaxBytes() { const maxMb = Number(process.env.UPLOAD_MAX_MB ?? 25); return maxMb * 1024 * 1024; } function hasMagicBytes(buffer: Buffer, mime: string, extension: string) { if (mime === "image/png") { return buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])); } if (mime === "image/jpeg") { return buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff; } if (mime === "image/webp") { return buffer.subarray(0, 4).toString("ascii") === "RIFF" && buffer.subarray(8, 12).toString("ascii") === "WEBP"; } if (mime === "application/pdf") { return buffer.subarray(0, 4).toString("ascii") === "%PDF"; } if (mime === "video/mp4" || mime === "video/quicktime") { return buffer.subarray(4, 8).toString("ascii") === "ftyp"; } if (extension === ".zip" || extension === ".docx" || extension === ".xlsx" || extension === ".pptx") { return buffer[0] === 0x50 && buffer[1] === 0x4b; } if (extension === ".doc" || extension === ".xls") { return buffer[0] === 0xd0 && buffer[1] === 0xcf && buffer[2] === 0x11 && buffer[3] === 0xe0; } if (mime === "image/svg+xml") { const start = buffer.subarray(0, 512).toString("utf8").trimStart().toLowerCase(); return start.startsWith(" maxBytes) { throw new Error(`Arquivo maior que o limite de ${Math.round(maxBytes / 1024 / 1024)} MB.`); } const buffer = Buffer.from(await file.arrayBuffer()); if (!hasMagicBytes(buffer, mime, extension)) { throw new Error("Conteudo do arquivo nao confere com o tipo informado."); } const uploadDir = getUploadDir(); await mkdir(uploadDir, { recursive: true }); const storedName = `${randomUUID()}${extension}`; const storedPath = path.join(uploadDir, storedName); await writeFile(storedPath, buffer); return { originalName, storedPath, mime, size: file.size }; } export async function removeStoredFile(filePath: string) { try { await unlink(filePath); } catch { // A ausencia do arquivo nao deve impedir a remocao do registro. } }