49 lines
1.7 KiB
TypeScript
49 lines
1.7 KiB
TypeScript
import { existsSync, statSync } from 'node:fs';
|
|||
|
|
import { basename, isAbsolute, join, relative, resolve } from 'node:path';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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(process.env.UPLOAD_DIR ?? join(process.cwd(), 'storage', 'uploads'));
|
||
|
|
}
|
||
|
|
|
||
|
|
const UPLOAD_MIME: Record<string, string> = {
|
||
|
|
'.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;
|
||
|
|
}
|