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 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) { return c.json({ error: `File not found or not ready. Status: ${status}, FileId: ${fileId}` }, 404); } 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)) { return c.json({ error: `File directory not found: ${fileDir}` }, 404); } // 读取目录内容,找到实际的文件(排除 .json 文件) const files = fs.readdirSync(fileDir).filter((f) => !f.endsWith('.json')); 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); // 获取文件统计信息 const stats = fs.statSync(filePath); const fileSize = stats.size; // 设置响应头 c.header('Content-Type', resource.type || 'application/octet-stream'); c.header('Content-Length', fileSize.toString()); c.header('Content-Disposition', `inline; filename="${actualFileName}"`); // 返回文件流 const fileStream = fs.createReadStream(filePath); return new Response(fileStream as any); } catch (error) { console.error('Error reading local file:', error); return c.json({ error: 'Failed to read file' }, 500); } } else if (storageType === StorageType.S3) { // S3 存储:通过已配置的dataStore获取文件信息 const dataStore = storageManager.getDataStore(); try { // 对于S3存储,我们需要根据fileId构建完整路径 // 由于S3Store的client是私有的,我们先尝试通过getUpload来验证文件存在 await (dataStore as any).getUpload(fileId + '/dummy'); // 这会失败,但能验证连接 } catch (error: any) { // 如果是FILE_NOT_FOUND以外的错误,说明连接有问题 if (error.message && !error.message.includes('FILE_NOT_FOUND')) { console.error('S3 connection error:', error); return c.json({ error: 'Failed to access S3 storage' }, 500); } } // 构建S3 URL - 使用resource信息重建完整路径 // 这里我们假设文件名就是resource.title const config = storageManager.getStorageConfig(); const s3Config = config.s3!; const fileName = resource.title || 'file'; const fullS3Key = `${fileId}/${fileName}`; // 生成 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}`); // 重定向到 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; }