rnotes-online/src/app/api/uploads/[filename]/route.ts

71 lines
2.1 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import { readFile } from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/app/uploads';
const MIME_TYPES: Record<string, string> = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.svg': 'image/svg+xml',
'.avif': 'image/avif',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.md': 'text/markdown',
'.csv': 'text/csv',
'.json': 'application/json',
'.xml': 'application/xml',
'.zip': 'application/zip',
'.gz': 'application/gzip',
'.js': 'text/javascript',
'.ts': 'text/typescript',
'.html': 'text/html',
'.css': 'text/css',
'.py': 'text/x-python',
};
export async function GET(
_request: NextRequest,
{ params }: { params: { filename: string } }
) {
try {
const filename = params.filename;
// Validate filename (no path traversal)
if (filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 });
}
const filePath = path.join(UPLOAD_DIR, filename);
// Validate resolved path stays within UPLOAD_DIR
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(path.resolve(UPLOAD_DIR))) {
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 });
}
if (!existsSync(filePath)) {
return NextResponse.json({ error: 'File not found' }, { status: 404 });
}
const data = await readFile(filePath);
const ext = path.extname(filename).toLowerCase();
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
return new NextResponse(data, {
headers: {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=31536000, immutable',
'Content-Length': data.length.toString(),
},
});
} catch (error) {
console.error('Serve file error:', error);
return NextResponse.json({ error: 'Failed to serve file' }, { status: 500 });
}
}