Files
compor-academy/server/src/uploads/asset.ts
Cauê Faleiros 6981092589
All checks were successful
CI / Validate frontend and API (push) Successful in 2m1s
CI / Build and publish Docker images (push) Successful in 21s
feat: upload course materials to bunny storage
2026-09-09 14:16:00 -03:00

53 lines
2.8 KiB
TypeScript

type AssetKind = 'document' | 'spreadsheet' | 'archive';
type AssetDefinition = {
kind: AssetKind;
extension: string;
needsZipSignature?: boolean;
needsOleSignature?: boolean;
needsPdfSignature?: boolean;
needsTextContent?: boolean;
};
const allowedAssetTypes = new Map<string, AssetDefinition>([
['application/pdf', { kind: 'document', extension: 'pdf', needsPdfSignature: true }],
['application/vnd.openxmlformats-officedocument.wordprocessingml.document', { kind: 'document', extension: 'docx', needsZipSignature: true }],
['application/msword', { kind: 'document', extension: 'doc', needsOleSignature: true }],
['application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', { kind: 'spreadsheet', extension: 'xlsx', needsZipSignature: true }],
['application/vnd.ms-excel', { kind: 'spreadsheet', extension: 'xls', needsOleSignature: true }],
['text/csv', { kind: 'spreadsheet', extension: 'csv', needsTextContent: true }],
['application/zip', { kind: 'archive', extension: 'zip', needsZipSignature: true }],
]);
export class AssetUploadValidationError extends Error {
constructor(message: string, public readonly statusCode: 400 | 413 | 415) {
super(message);
}
}
const zipSignature = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
const oleSignature = Buffer.from([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]);
export function validateAssetUpload(body: Buffer, contentType: string | undefined, maxBytes: number) {
const normalizedType = contentType?.split(';')[0]?.trim().toLowerCase();
const definition = normalizedType ? allowedAssetTypes.get(normalizedType) : undefined;
if (!definition) {
throw new AssetUploadValidationError('Envie apenas arquivos PDF, DOCX, XLSX, CSV ou ZIP.', 415);
}
if (body.length === 0) throw new AssetUploadValidationError('Escolha um arquivo para enviar.', 400);
if (body.length > maxBytes) throw new AssetUploadValidationError(`O arquivo excede o limite de ${Math.floor(maxBytes / 1024 / 1024)} MB.`, 413);
if (definition.needsPdfSignature && !body.subarray(0, 5).equals(Buffer.from('%PDF-'))) {
throw new AssetUploadValidationError('O arquivo enviado não é um PDF válido.', 415);
}
if (definition.needsZipSignature && !body.subarray(0, 4).equals(zipSignature)) {
throw new AssetUploadValidationError('O arquivo enviado não possui o formato esperado.', 415);
}
if (definition.needsOleSignature && !body.subarray(0, 8).equals(oleSignature)) {
throw new AssetUploadValidationError('O arquivo enviado não possui o formato esperado.', 415);
}
if (definition.needsTextContent && body.subarray(0, Math.min(body.length, 4096)).includes(0)) {
throw new AssetUploadValidationError('O arquivo CSV deve conter texto.', 415);
}
return { kind: definition.kind, extension: definition.extension, contentType: normalizedType as string };
}