Files
fosterflow/src/lib/server/upload.ts
T

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
}
}