/** * 远程文件代理控制器模块 * 提供远程文件的 HTTP 直链代理访问功能 * @module file-proxy-controller */ import { createHealthResponse, createStatusResponse, decodeParam, forwardResponseHeaders, getDefaultHeaders, makeRemoteRequest, PROXY_CONSTANTS, setCorsHeaders, verifyAuth } from '../utils/proxy-util.js'; /** * 远程文件代理控制器插件 * @param {Object} fastify - Fastify实例 * @param {Object} options - 插件选项 * @param {Function} done - 完成回调 */ export default (fastify, options, done) => { // 请求缓存 const requestCache = new Map(); /** * 远程文件代理健康检查接口 * GET /file-proxy/health - 检查远程文件代理服务状态 */ fastify.get('/file-proxy/health', async (request, reply) => { // console.log(`[fileProxyController] Health check request`); setCorsHeaders(reply); const healthData = createHealthResponse('Remote File Proxy', { cache: { requests: requestCache.size } }); return reply.send(healthData); }); /** * 远程文件代理接口 * GET/HEAD /file-proxy/proxy - 代理远程文件 */ fastify.route({ method: ['GET', 'HEAD'], url: '/file-proxy/proxy', handler: async (request, reply) => { // 验证身份认证 if (!verifyAuth(request, reply)) { return; } const { url: urlParam, headers: headersParam } = request.query; // console.log(`[fileProxyController] ${request.method} request for URL: ${urlParam}`); // 验证必需参数 if (!urlParam) { return reply.status(400).send({ error: 'Missing required parameter: url' }); } try { // 解码 URL 参数 const targetUrl = decodeParam(urlParam, false); // 验证 URL 格式 if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { return reply.status(400).send({ error: 'Invalid URL: must start with http:// or https://' }); } // 解码 headers 参数 const customHeaders = decodeParam(headersParam, true); // 合并默认请求头和自定义请求头 const defaultHeaders = getDefaultHeaders(request); const requestHeaders = { ...defaultHeaders, ...customHeaders }; // 处理 Range 请求 const range = request.headers.range; try { // 发起远程请求 const remoteResponse = await makeRemoteRequest( targetUrl, requestHeaders, request.method, range ); // 检查远程响应状态 if (remoteResponse.statusCode >= 400) { return reply.status(remoteResponse.statusCode).send({ error: `Remote server error: ${remoteResponse.statusCode}` }); } // 设置响应状态码 reply.status(remoteResponse.statusCode); // 转发响应头和设置CORS forwardResponseHeaders(reply, remoteResponse.headers); setCorsHeaders(reply); // 对于 HEAD 请求,只返回头部信息 if (request.method === 'HEAD') { return reply.send(); } // 对于 GET 请求,返回文件流 return reply.send(remoteResponse.stream); } catch (requestError) { console.error('[fileProxyController] Remote request error:', requestError); return reply.status(502).send({ error: `Failed to fetch remote file: ${requestError.message}` }); } } catch (error) { console.error('[fileProxyController] Request processing error:', error); return reply.status(500).send({ error: error.message }); } } }); /** * 远程文件信息获取接口 * GET /file-proxy/info - 获取远程文件信息(HEAD 请求) */ fastify.get('/file-proxy/info', async (request, reply) => { // 验证身份认证 if (!verifyAuth(request, reply)) { return; } const { url: urlParam, headers: headersParam } = request.query; // console.log(`[fileProxyController] Info request for URL: ${urlParam}`); // 验证必需参数 if (!urlParam) { return reply.status(400).send({ error: 'Missing required parameter: url' }); } try { // 解码 URL 参数 const targetUrl = decodeParam(urlParam, false); // 验证 URL 格式 if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { return reply.status(400).send({ error: 'Invalid URL: must start with http:// or https://' }); } // 解码 headers 参数 const customHeaders = decodeParam(headersParam, true); // 合并默认请求头和自定义请求头 const defaultHeaders = getDefaultHeaders(request); const requestHeaders = { ...defaultHeaders, ...customHeaders }; try { // 发起 HEAD 请求获取文件信息 const remoteResponse = await makeRemoteRequest(targetUrl, requestHeaders, 'HEAD'); if (remoteResponse.statusCode >= 400) { return reply.status(remoteResponse.statusCode).send({ error: `Remote server error: ${remoteResponse.statusCode}` }); } // 提取文件信息 const fileInfo = { url: targetUrl, statusCode: remoteResponse.statusCode, contentType: remoteResponse.headers['content-type'] || 'application/octet-stream', contentLength: remoteResponse.headers['content-length'] ? parseInt(remoteResponse.headers['content-length']) : null, lastModified: remoteResponse.headers['last-modified'] || null, etag: remoteResponse.headers['etag'] || null, acceptRanges: remoteResponse.headers['accept-ranges'] || null, cacheControl: remoteResponse.headers['cache-control'] || null, expires: remoteResponse.headers['expires'] || null }; return reply.send(fileInfo); } catch (requestError) { console.error('[fileProxyController] Remote info request error:', requestError); return reply.status(502).send({ error: `Failed to get remote file info: ${requestError.message}` }); } } catch (error) { console.error('[fileProxyController] Info request processing error:', error); return reply.status(500).send({ error: error.message }); } }); /** * 清理缓存路由 */ fastify.delete('/file-proxy/cache', async (request, reply) => { if (!verifyAuth(request, reply)) return; setCorsHeaders(reply); const beforeSize = requestCache.size; requestCache.clear(); reply.send({ status: 'success', message: 'Cache cleared', cleared: beforeSize, timestamp: new Date().toISOString() }); }); /** * 状态路由 */ fastify.get('/file-proxy/status', async (request, reply) => { setCorsHeaders(reply); const statusData = createStatusResponse('file-proxy', '1.0.0', { cache: { size: requestCache.size, timeout: PROXY_CONSTANTS.CACHE_TIMEOUT } }); reply.send(statusData); }); done(); };