133 lines
3.9 KiB
TypeScript
133 lines
3.9 KiB
TypeScript
// 文件访问API - 提供本地上传文件的访问
|
||
import { NextRequest } from 'next/server';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import { lookup as mimeTypeLookup } from 'mime-types';
|
||
|
||
export const runtime = 'nodejs';
|
||
export const dynamic = 'force-dynamic';
|
||
|
||
// GET - 获取文件
|
||
export async function GET(
|
||
request: NextRequest,
|
||
{ params }: { params: { path: string[] } }
|
||
) {
|
||
const pathSegments = params.path || [];
|
||
|
||
// 健康检查
|
||
if (pathSegments.length === 1 && pathSegments[0] === 'health') {
|
||
return new Response('OK', { status: 200 });
|
||
}
|
||
|
||
const uploadDir = process.env.UPLOAD_DIR || '/app/uploads';
|
||
const relativePath = path.join(...pathSegments);
|
||
const filePath = path.resolve(uploadDir, relativePath);
|
||
const baseDir = path.resolve(uploadDir);
|
||
|
||
// 安全检查:防止路径遍历攻击
|
||
if (!filePath.startsWith(baseDir)) {
|
||
return new Response('Forbidden', { status: 403 });
|
||
}
|
||
|
||
try {
|
||
const stats = await fs.promises.stat(filePath);
|
||
|
||
if (!stats.isFile()) {
|
||
return new Response('Not Found', { status: 404 });
|
||
}
|
||
|
||
const range = request.headers.get('range');
|
||
const contentType = mimeTypeLookup(path.extname(filePath)) || 'application/octet-stream';
|
||
|
||
// 支持Range请求(用于视频流)
|
||
if (range) {
|
||
const fileSize = stats.size;
|
||
const parts = range.match(/bytes=(\d*)-(\d*)/);
|
||
const start = parts && parts[1] ? parseInt(parts[1], 10) : 0;
|
||
const end = parts && parts[2] ? parseInt(parts[2], 10) : fileSize - 1;
|
||
|
||
// 验证Range
|
||
if (isNaN(start) || isNaN(end) || start > end || end >= fileSize) {
|
||
return new Response('Invalid Range', { status: 416 });
|
||
}
|
||
|
||
const stream = fs.createReadStream(filePath, { start, end });
|
||
|
||
return new Response(stream as any, {
|
||
status: 206,
|
||
headers: {
|
||
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
|
||
'Accept-Ranges': 'bytes',
|
||
'Content-Length': String(end - start + 1),
|
||
'Content-Type': contentType,
|
||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||
},
|
||
});
|
||
}
|
||
|
||
// 普通请求
|
||
const stream = fs.createReadStream(filePath);
|
||
|
||
return new Response(stream as any, {
|
||
status: 200,
|
||
headers: {
|
||
'Content-Length': String(stats.size),
|
||
'Content-Type': contentType,
|
||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||
},
|
||
});
|
||
} catch (error: any) {
|
||
if (error.code === 'ENOENT') {
|
||
return new Response('Not Found', { status: 404 });
|
||
}
|
||
return new Response('Server Error', { status: 500 });
|
||
}
|
||
}
|
||
|
||
// HEAD - 检查文件是否存在
|
||
export async function HEAD(
|
||
request: NextRequest,
|
||
{ params }: { params: { path: string[] } }
|
||
) {
|
||
const pathSegments = params.path || [];
|
||
|
||
// 健康检查
|
||
if (pathSegments.length === 1 && pathSegments[0] === 'health') {
|
||
return new Response(null, { status: 200 });
|
||
}
|
||
|
||
const uploadDir = process.env.UPLOAD_DIR || '/app/uploads';
|
||
const relativePath = path.join(...pathSegments);
|
||
const filePath = path.resolve(uploadDir, relativePath);
|
||
const baseDir = path.resolve(uploadDir);
|
||
|
||
// 安全检查:防止路径遍历攻击
|
||
if (!filePath.startsWith(baseDir)) {
|
||
return new Response(null, { status: 403 });
|
||
}
|
||
|
||
try {
|
||
const stats = await fs.promises.stat(filePath);
|
||
|
||
if (!stats.isFile()) {
|
||
return new Response(null, { status: 404 });
|
||
}
|
||
|
||
const contentType = mimeTypeLookup(path.extname(filePath)) || 'application/octet-stream';
|
||
|
||
return new Response(null, {
|
||
status: 200,
|
||
headers: {
|
||
'Content-Length': String(stats.size),
|
||
'Content-Type': contentType,
|
||
'Cache-Control': 'public, max-age=31536000, immutable',
|
||
},
|
||
});
|
||
} catch (error: any) {
|
||
if (error.code === 'ENOENT') {
|
||
return new Response(null, { status: 404 });
|
||
}
|
||
return new Response(null, { status: 500 });
|
||
}
|
||
}
|