import { Hono } from 'hono'; import { handleTusRequest, cleanupExpiredUploads, getStorageInfo } from '../services/tus'; import { getResourceByFileId, deleteResource, updateResource, migrateResourcesStorageType, } from '../database/operations'; import { StorageManager, validateStorageConfig } from '../core/adapter'; import { StorageType, type StorageConfig } from '../types'; /** * 创建存储相关的 Hono 路由 * @param basePath 基础路径,默认为 '/api/storage' * @returns Hono 应用实例 */ export function createStorageRoutes(basePath: string = '/api/storage') { const app = new Hono(); // 获取文件资源信息 app.get('/resource/:fileId', async (c) => { const encodedFileId = c.req.param('fileId'); const fileId = decodeURIComponent(encodedFileId); console.log('API request - Encoded fileId:', encodedFileId); console.log('API request - Decoded fileId:', fileId); const result = await getResourceByFileId(fileId); return c.json(result); }); // 删除资源 app.delete('/resource/:id', async (c) => { const id = c.req.param('id'); const result = await deleteResource(id); return c.json(result); }); // 更新资源 app.patch('/resource/:id', async (c) => { const id = c.req.param('id'); const data = await c.req.json(); const result = await updateResource(id, data); return c.json(result); }); // 迁移资源存储类型(批量更新数据库中的存储类型标记) app.post('/migrate-storage', async (c) => { try { const { from, to } = await c.req.json(); const result = await migrateResourcesStorageType(from as StorageType, to as StorageType); return c.json({ success: true, message: `Migrated ${result.count} resources from ${from} to ${to}`, count: result.count, }); } catch (error) { console.error('Failed to migrate storage type:', error); return c.json( { success: false, error: error instanceof Error ? error.message : 'Unknown error', }, 400, ); } }); // 清理过期上传 app.post('/cleanup', async (c) => { const result = await cleanupExpiredUploads(); return c.json(result); }); // 获取存储信息 app.get('/storage/info', async (c) => { const storageInfo = getStorageInfo(); return c.json(storageInfo); }); // 切换存储类型(需要重启应用) app.post('/storage/switch', async (c) => { try { const newConfig = (await c.req.json()) as StorageConfig; const storageManager = StorageManager.getInstance(); await storageManager.switchStorage(newConfig); return c.json({ success: true, message: 'Storage configuration updated. Please restart the application for changes to take effect.', newType: newConfig.type, }); } catch (error) { console.error('Failed to switch storage:', error); return c.json( { success: false, error: error instanceof Error ? error.message : 'Unknown error', }, 400, ); } }); // 验证存储配置 app.post('/storage/validate', async (c) => { try { const config = (await c.req.json()) as StorageConfig; const errors = validateStorageConfig(config); if (errors.length > 0) { return c.json({ valid: false, errors }, 400); } return c.json({ valid: true, message: 'Storage configuration is valid' }); } catch (error) { return c.json( { valid: false, errors: [error instanceof Error ? error.message : 'Invalid JSON'], }, 400, ); } }); return app; } /** * 创建TUS上传处理路由 * @param uploadPath 上传路径,默认为 '/upload' * @returns Hono 应用实例 */ export function createTusUploadRoutes(uploadPath: string = '/upload') { const app = new Hono(); // TUS 协议处理 - 使用通用处理器 app.all('/*', async (c) => { try { // 创建适配的请求和响应对象 const adaptedReq = createNodeRequestAdapter(c); const adaptedRes = createNodeResponseAdapter(c); await handleTusRequest(adaptedReq, adaptedRes); return adaptedRes.getResponse(); } catch (error) { console.error('TUS request error:', error); return c.json({ error: 'Upload request failed' }, 500); } }); return app; } // Node.js 请求适配器 function createNodeRequestAdapter(c: any) { const honoReq = c.req; const url = new URL(honoReq.url); // 导入Node.js模块 const { Readable } = require('stream'); const { EventEmitter } = require('events'); // 创建一个继承自Readable的适配器类 class TusRequestAdapter extends Readable { method: string; url: string; headers: Record; httpVersion: string; complete: boolean; private reader: ReadableStreamDefaultReader | null = null; private _reading: boolean = false; constructor() { super(); this.method = honoReq.method; this.url = url.pathname + url.search; this.headers = honoReq.header() || {}; this.httpVersion = '1.1'; this.complete = false; // 如果有请求体,获取reader if (honoReq.method !== 'GET' && honoReq.method !== 'HEAD' && honoReq.raw.body) { this.reader = honoReq.raw.body.getReader(); } } _read() { if (this._reading || !this.reader) { return; } this._reading = true; this.reader .read() .then(({ done, value }) => { this._reading = false; if (done) { this.push(null); // 结束流 this.complete = true; } else { // 确保我们推送的是正确的二进制数据 const buffer = Buffer.from(value); this.push(buffer); } }) .catch((error) => { this._reading = false; this.destroy(error); }); } // 模拟IncomingMessage的destroy方法 destroy(error?: Error) { if (this.reader) { this.reader.cancel().catch(() => { // 忽略取消错误 }); this.reader = null; } super.destroy(error); } } return new TusRequestAdapter(); } // Node.js 响应适配器 function createNodeResponseAdapter(c: any) { let statusCode = 200; let headers: Record = {}; let body: any = null; const adapter = { statusCode, setHeader: (name: string, value: string) => { headers[name] = value; }, getHeader: (name: string) => { return headers[name]; }, writeHead: (code: number, reasonOrHeaders?: any, headersObj?: any) => { statusCode = code; if (typeof reasonOrHeaders === 'object') { Object.assign(headers, reasonOrHeaders); } if (headersObj) { Object.assign(headers, headersObj); } }, write: (chunk: any) => { if (body === null) { body = chunk; } else if (typeof body === 'string' && typeof chunk === 'string') { body += chunk; } else { // 处理 Buffer 或其他类型 body = chunk; } }, end: (data?: any) => { if (data !== undefined) { body = data; } }, // 添加事件方法 on: (event: string, handler: Function) => { // 简单的空实现 }, emit: (event: string, ...args: any[]) => { // 简单的空实现 }, // 获取最终的 Response 对象 getResponse: () => { if (body === null || body === undefined) { return new Response(null, { status: statusCode, headers: headers, }); } return new Response(body, { status: statusCode, headers: headers, }); }, }; return adapter; } /** * 创建文件下载路由(支持所有存储类型) * @param downloadPath 下载路径,默认为 '/download' * @returns Hono 应用实例 */ export function createFileDownloadRoutes(downloadPath: string = '/download') { const app = new Hono(); // 通过文件ID下载文件 app.get('/:fileId', async (c) => { try { // 获取并解码fileId const encodedFileId = c.req.param('fileId'); const fileId = decodeURIComponent(encodedFileId); console.log('=== DOWNLOAD DEBUG START ==='); console.log('Download request - Encoded fileId:', encodedFileId); console.log('Download request - Decoded fileId:', fileId); const storageManager = StorageManager.getInstance(); const storageType = storageManager.getStorageType(); // 从数据库获取文件信息 const { status, resource } = await getResourceByFileId(fileId); if (status !== 'UPLOADED' || !resource) { console.log('Download - File not found, status:', status); return c.json({ error: `File not found or not ready. Status: ${status}, FileId: ${fileId}` }, 404); } // 详细记录资源信息 console.log('Download - Full resource object:', JSON.stringify(resource, null, 2)); console.log('Download - Resource title:', resource.title); console.log('Download - Resource type:', resource.type); console.log('Download - Resource fileId:', resource.fileId); // 使用resource.title作为下载文件名,如果没有则使用默认名称 let downloadFileName = resource.title || 'download'; // 确保文件名有正确的扩展名 if (downloadFileName && !downloadFileName.includes('.') && resource.type) { // 如果没有扩展名,尝试从MIME类型推断 const mimeTypeToExt: Record = { // Microsoft Office 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', 'application/msword': '.doc', 'application/vnd.ms-excel': '.xls', 'application/vnd.ms-powerpoint': '.ppt', // WPS Office 'application/wps-office.docx': '.docx', 'application/wps-office.xlsx': '.xlsx', 'application/wps-office.pptx': '.pptx', 'application/wps-office.doc': '.doc', 'application/wps-office.xls': '.xls', 'application/wps-office.ppt': '.ppt', // 其他文档格式 'application/pdf': '.pdf', 'application/rtf': '.rtf', 'text/plain': '.txt', 'text/csv': '.csv', 'application/json': '.json', 'application/xml': '.xml', 'text/xml': '.xml', // 图片格式 'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/png': '.png', 'image/gif': '.gif', 'image/bmp': '.bmp', 'image/webp': '.webp', 'image/svg+xml': '.svg', 'image/tiff': '.tiff', // 音频格式 'audio/mpeg': '.mp3', 'audio/wav': '.wav', 'audio/ogg': '.ogg', 'audio/aac': '.aac', 'audio/flac': '.flac', // 视频格式 'video/mp4': '.mp4', 'video/avi': '.avi', 'video/quicktime': '.mov', 'video/x-msvideo': '.avi', 'video/webm': '.webm', // 压缩文件 'application/zip': '.zip', 'application/x-rar-compressed': '.rar', 'application/x-7z-compressed': '.7z', 'application/gzip': '.gz', 'application/x-tar': '.tar', // 其他常见格式 'application/octet-stream': '', }; const extension = mimeTypeToExt[resource.type]; if (extension) { downloadFileName += extension; console.log('Download - Added extension from MIME type:', extension); } } console.log('Download - Final download filename:', downloadFileName); if (storageType === StorageType.LOCAL) { // 本地存储:直接读取文件 const config = storageManager.getStorageConfig(); const uploadDir = config.local?.directory || './uploads'; // fileId 是目录路径格式,直接使用 const fileDir = `${uploadDir}/${fileId}`; try { // 使用 Node.js fs 而不是 Bun.file const fs = await import('fs'); const path = await import('path'); // 检查目录是否存在 if (!fs.existsSync(fileDir)) { console.log('Download - Directory not found:', fileDir); return c.json({ error: `File directory not found: ${fileDir}` }, 404); } // 读取目录内容,找到实际的文件(排除 .json 文件) const files = fs.readdirSync(fileDir).filter((f) => !f.endsWith('.json')); console.log('Download - Files in directory:', files); if (files.length === 0) { return c.json({ error: `No file found in directory: ${fileDir}` }, 404); } // 通常只有一个文件,取第一个 const actualFileName = files[0]; if (!actualFileName) { return c.json({ error: 'No valid file found' }, 404); } const filePath = path.join(fileDir, actualFileName); console.log('Download - Actual file in directory:', actualFileName); console.log('Download - Full file path:', filePath); // 获取文件统计信息 const stats = fs.statSync(filePath); const fileSize = stats.size; // 强制设置正确的MIME类型 let contentType = resource.type || 'application/octet-stream'; if (downloadFileName.endsWith('.docx')) { contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; } else if (downloadFileName.endsWith('.xlsx')) { contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; } else if (downloadFileName.endsWith('.pdf')) { contentType = 'application/pdf'; } console.log('Download - Final Content-Type:', contentType); // 处理中文文件名 - 现在使用正确的RFC 2231格式 let contentDisposition: string; const hasNonAscii = !/^[\x00-\x7F]*$/.test(downloadFileName); if (hasNonAscii) { // 包含中文字符,使用RFC 2231标准 const encodedFileName = encodeURIComponent(downloadFileName); // 同时提供fallback和UTF-8编码版本 const fallbackName = downloadFileName.replace(/[^\x00-\x7F]/g, '_'); contentDisposition = `attachment; filename="${fallbackName}"; filename*=UTF-8''${encodedFileName}`; console.log('Download - Original filename:', downloadFileName); console.log('Download - Encoded filename:', encodedFileName); console.log('Download - Fallback filename:', fallbackName); } else { // ASCII文件名,使用简单格式 contentDisposition = `attachment; filename="${downloadFileName}"`; } // 设置所有必要的响应头 c.header('Content-Type', contentType); c.header('Content-Length', fileSize.toString()); c.header('Content-Disposition', contentDisposition); // 添加额外的头部以确保浏览器正确处理 c.header('Cache-Control', 'no-cache, no-store, must-revalidate'); c.header('Pragma', 'no-cache'); c.header('Expires', '0'); console.log('Download - Content-Disposition:', contentDisposition); console.log('=== DOWNLOAD DEBUG END ==='); // 返回文件流 - 使用Hono的正确方式 const fileStream = fs.createReadStream(filePath); // 将Node.js ReadStream转换为Web Stream const readableStream = new ReadableStream({ start(controller) { fileStream.on('data', (chunk) => { controller.enqueue(chunk); }); fileStream.on('end', () => { controller.close(); }); fileStream.on('error', (error) => { controller.error(error); }); }, }); return new Response(readableStream, { status: 200, headers: { 'Content-Type': contentType, 'Content-Length': fileSize.toString(), 'Content-Disposition': contentDisposition, 'Cache-Control': 'no-cache, no-store, must-revalidate', Pragma: 'no-cache', Expires: '0', }, }); } catch (error) { console.error('Error reading local file:', error); return c.json({ error: 'Failed to read file' }, 500); } } else if (storageType === StorageType.S3) { // S3 存储:简单重定向,让S3处理文件名 const config = storageManager.getStorageConfig(); const s3Config = config.s3!; // 构建S3 key - 使用fileId和原始文件名 const fileName = resource.title || 'file'; const fullS3Key = `${fileId}/${fileName}`; console.log('Download - S3 Key:', fullS3Key); // 生成 S3 URL let s3Url: string; if (s3Config.endpoint && s3Config.endpoint !== 'https://s3.amazonaws.com') { // 自定义 S3 兼容服务 s3Url = `${s3Config.endpoint}/${s3Config.bucket}/${fullS3Key}`; } else { // AWS S3 s3Url = `https://${s3Config.bucket}.s3.${s3Config.region}.amazonaws.com/${fullS3Key}`; } console.log(`Redirecting to S3 URL: ${s3Url}`); console.log('=== DOWNLOAD DEBUG END ==='); // 重定向到 S3 URL return c.redirect(s3Url, 302); } return c.json({ error: 'Unsupported storage type' }, 500); } catch (error) { console.error('Download error:', error); return c.json({ error: 'Internal server error' }, 500); } }); return app; } /** * 创建完整的存储应用,包含API和上传功能 * @param options 配置选项 * @returns Hono 应用实例 */ export function createStorageApp( options: { apiBasePath?: string; uploadPath?: string; downloadPath?: string; } = {}, ) { const { apiBasePath = '/api/storage', uploadPath = '/upload', downloadPath = '/download' } = options; const app = new Hono(); // 添加存储API路由 app.route(apiBasePath, createStorageRoutes()); // 添加TUS上传路由 app.route(uploadPath, createTusUploadRoutes()); // 添加文件下载路由 app.route(downloadPath, createFileDownloadRoutes()); return app; }