Files

135 lines
3.5 KiB
TypeScript

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("<svg") || start.startsWith("<?xml");
}
return false;
}
export async function validateAndStoreUpload(file: File) {
const originalName = file.name;
const extension = path.extname(originalName).toLowerCase();
const mime = file.type;
const maxBytes = getUploadMaxBytes();
if (!allowedExtensions.has(extension)) {
throw new Error("Tipo de arquivo nao permitido.");
}
if (!allowedMimes.has(mime)) {
throw new Error("MIME do arquivo nao permitido.");
}
if (file.size <= 0) {
throw new Error("Arquivo vazio.");
}
if (file.size > 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.
}
}