133 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 文件访问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 });
}
}