import { existsSync, statSync } from 'node:fs'; import { basename, isAbsolute, join, relative, resolve } from 'node:path'; import { envOr } from '../../common/env'; /** * Directory files are written to and served from. * * Default matches docker-compose's `api_uploads:/app/storage/uploads` volume. * A nested leftover (`storage/uploads/uploads/`) is also checked when serving, * because copying a folder named `uploads` into the volume creates that extra * directory and the previous static root then 404'd every file. */ export function getUploadDir(): string { return resolve(envOr('UPLOAD_DIR', join(process.cwd(), 'storage', 'uploads'))); } const UPLOAD_MIME: Record = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.avif': 'image/avif', '.svg': 'image/svg+xml', '.pdf': 'application/pdf', }; export function mimeForUpload(filePath: string): string | undefined { const dot = filePath.lastIndexOf('.'); const ext = dot >= 0 ? filePath.slice(dot).toLowerCase() : ''; return UPLOAD_MIME[ext]; } /** Resolve a public filename to an on-disk file, or null if it is missing/unsafe. */ export function resolveStoredUpload(rawName: string): string | null { const safe = basename(rawName); if (!safe || safe === '.' || safe === '..') return null; const dir = getUploadDir(); const candidates = [join(dir, safe), join(dir, 'uploads', safe)]; for (const candidate of candidates) { const resolved = resolve(candidate); const rel = relative(dir, resolved); if (!rel || rel.startsWith('..') || isAbsolute(rel)) continue; if (existsSync(resolved) && statSync(resolved).isFile()) return resolved; } return null; }