442 lines
12 KiB
TypeScript
442 lines
12 KiB
TypeScript
![]() |
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;
|
|||
|
}
|