fenghuo/packages/storage/src/middleware/hono.ts

442 lines
12 KiB
TypeScript
Raw Normal View History

2025-05-28 20:00:36 +08:00
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<string, string>;
httpVersion: string;
complete: boolean;
private reader: ReadableStreamDefaultReader<Uint8Array> | 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<string, string> = {};
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;
}