fenghuo/apps/backend/src/upload/tus.ts

154 lines
4.5 KiB
TypeScript

import { Server, Upload } from '@repo/tus';
import { prisma } from '@repo/db';
import { getFilenameWithoutExt } from '../utils/file';
import { nanoid } from 'nanoid';
import { slugify } from 'transliteration';
import { StorageManager } from './storage.adapter';
const FILE_UPLOAD_CONFIG = {
maxSizeBytes: 20_000_000_000, // 20GB
};
export enum QueueJobType {
UPDATE_STATS = 'update_stats',
FILE_PROCESS = 'file_process',
UPDATE_POST_VISIT_COUNT = 'updatePostVisitCount',
UPDATE_POST_STATE = 'updatePostState',
}
export enum ResourceStatus {
UPLOADING = 'UPLOADING',
UPLOADED = 'UPLOADED',
PROCESS_PENDING = 'PROCESS_PENDING',
PROCESSING = 'PROCESSING',
PROCESSED = 'PROCESSED',
PROCESS_FAILED = 'PROCESS_FAILED',
}
// 全局 TUS 服务器实例
let tusServer: Server | null = null;
function getFileId(uploadId: string) {
return uploadId.replace(/\/[^/]+$/, '');
}
async function handleUploadCreate(req: any, res: any, upload: Upload, url: string) {
try {
const fileId = getFileId(upload.id);
const storageManager = StorageManager.getInstance();
await prisma.resource.create({
data: {
title: getFilenameWithoutExt(upload.metadata?.filename || 'untitled'),
fileId, // 移除最后的文件名
url: upload.id,
meta: upload.metadata,
status: ResourceStatus.UPLOADING,
storageType: storageManager.getStorageType(), // 记录存储类型
},
});
console.log(`Resource created for ${upload.id} using ${storageManager.getStorageType()} storage`);
} catch (error) {
console.error('Failed to create resource during upload', error);
}
}
async function handleUploadFinish(req: any, res: any, upload: Upload) {
try {
const resource = await prisma.resource.update({
where: { fileId: getFileId(upload.id) },
data: { status: ResourceStatus.UPLOADED },
});
// TODO: 这里可以添加队列处理逻辑
// fileQueue.add(QueueJobType.FILE_PROCESS, { resource }, { jobId: resource.id });
console.log(`Upload finished ${resource.url} using ${StorageManager.getInstance().getStorageType()} storage`);
} catch (error) {
console.error('Failed to update resource after upload', error);
}
}
function initializeTusServer() {
if (tusServer) {
return tusServer;
}
// 获取存储管理器实例
const storageManager = StorageManager.getInstance();
const dataStore = storageManager.getDataStore();
tusServer = new Server({
namingFunction(req, metadata) {
const safeFilename = slugify(metadata?.filename || 'untitled');
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const uniqueId = nanoid(10);
return `${year}/${month}/${day}/${uniqueId}/${safeFilename}`;
},
path: '/upload',
datastore: dataStore, // 使用存储适配器
maxSize: FILE_UPLOAD_CONFIG.maxSizeBytes,
postReceiveInterval: 1000,
getFileIdFromRequest: (req, lastPath) => {
const match = req.url?.match(/\/upload\/(.+)/);
return match ? match[1] : lastPath;
},
});
// 设置事件处理器
tusServer.on('POST_CREATE', handleUploadCreate);
tusServer.on('POST_FINISH', handleUploadFinish);
console.log(`TUS server initialized with ${storageManager.getStorageType()} storage`);
return tusServer;
}
export function getTusServer() {
return initializeTusServer();
}
export async function handleTusRequest(req: any, res: any) {
const server = getTusServer();
return server.handle(req, res);
}
export async function cleanupExpiredUploads() {
try {
const storageManager = StorageManager.getInstance();
// 获取过期时间配置,如果设置为 0 则不自动清理
const expirationPeriod: number = 24 * 60 * 60 * 1000;
// Delete incomplete uploads older than expiration period
const deletedResources = await prisma.resource.deleteMany({
where: {
createdAt: {
lt: new Date(Date.now() - expirationPeriod),
},
status: ResourceStatus.UPLOADING,
},
});
const server = getTusServer();
const expiredUploadCount = await server.cleanUpExpiredUploads();
console.log(
`Cleanup complete: ${deletedResources.count} resources and ${expiredUploadCount} uploads removed from ${storageManager.getStorageType()} storage`,
);
return { deletedResources: deletedResources.count, expiredUploads: expiredUploadCount };
} catch (error) {
console.error('Expired uploads cleanup failed', error);
throw error;
}
}
// 获取存储信息
export function getStorageInfo() {
const storageManager = StorageManager.getInstance();
return storageManager.getStorageInfo();
}