154 lines
4.5 KiB
TypeScript
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();
|
|
}
|