67 lines
1.8 KiB
TypeScript
67 lines
1.8 KiB
TypeScript
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import crypto from 'crypto';
|
|
|
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads';
|
|
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
|
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
|
|
|
|
const MAGIC_BYTES: Record<string, number[]> = {
|
|
'image/jpeg': [0xff, 0xd8, 0xff],
|
|
'image/png': [0x89, 0x50, 0x4e, 0x47],
|
|
'image/gif': [0x47, 0x49, 0x46],
|
|
'image/webp': [0x52, 0x49, 0x46, 0x46]
|
|
};
|
|
|
|
function validateMagicBytes(buffer: Buffer, mimeType: string): boolean {
|
|
const expected = MAGIC_BYTES[mimeType];
|
|
if (!expected) return false;
|
|
for (let i = 0; i < expected.length; i++) {
|
|
if (buffer[i] !== expected[i]) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export async function saveUpload(
|
|
file: File,
|
|
subdir = 'general'
|
|
): Promise<{ url: string; path: string }> {
|
|
if (file.size > MAX_SIZE) {
|
|
throw new Error('File too large (max 10MB)');
|
|
}
|
|
|
|
if (!ALLOWED_TYPES.includes(file.type)) {
|
|
throw new Error('File type not allowed');
|
|
}
|
|
|
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
|
|
if (!validateMagicBytes(buffer, file.type)) {
|
|
throw new Error('File content does not match declared type');
|
|
}
|
|
|
|
const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg';
|
|
const filename = `${crypto.randomUUID()}.${ext}`;
|
|
const dir = path.join(UPLOAD_DIR, subdir);
|
|
|
|
await fs.mkdir(dir, { recursive: true });
|
|
|
|
const filePath = path.join(dir, filename);
|
|
await fs.writeFile(filePath, buffer);
|
|
|
|
return {
|
|
url: `/uploads/${subdir}/${filename}`,
|
|
path: filePath
|
|
};
|
|
}
|
|
|
|
export async function deleteUpload(url: string): Promise<void> {
|
|
if (!url.startsWith('/uploads/')) return;
|
|
const filePath = path.join(UPLOAD_DIR, url.replace('/uploads/', ''));
|
|
try {
|
|
await fs.unlink(filePath);
|
|
} catch {
|
|
// File already gone
|
|
}
|
|
}
|