/** * 清理临时上传文件的定时任务脚本 * 用于清理:1. 过期的上传会话对应的临时文件 2. 失败的上传临时文件 * * 运行方式: * 1. 手动运行:npx tsx scripts/cleanup-upload-temp.ts * 2. 定时任务:添加到 cron 或系统定时任务中 * * 建议:每小时运行一次 */ import { promises as fs } from 'fs' import path from 'path' import { getUploadSession } from '../lib/upload-sessions' const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads' const MAX_AGE_HOURS = 8 // 8小时后清理 const MAX_AGE = MAX_AGE_HOURS * 60 * 60 * 1000 // 转换为毫秒 async function cleanupUploadSession(uploadId: string): Promise<{ success: boolean deletedFiles: number freedSpace: number error?: string }> { const tempDir = path.join(UPLOAD_DIR, 'temp', uploadId) let deletedFiles = 0 let freedSpace = 0 try { // 检查目录是否存在 try { await fs.access(tempDir) } catch { // 目录不存在,说明已经清理过了 return { success: true, deletedFiles: 0, freedSpace: 0 } } // 读取目录中的所有文件 const files = await fs.readdir(tempDir) for (const file of files) { const filePath = path.join(tempDir, file) try { const stat = await fs.stat(filePath) if (stat.isFile()) { await fs.unlink(filePath) deletedFiles++ freedSpace += stat.size } } catch (err) { console.warn(`[Cleanup] 删除文件失败: ${filePath}`, err) } } // 删除目录本身 try { await fs.rmdir(tempDir) } catch (err) { console.warn(`[Cleanup] 删除目录失败: ${tempDir}`, err) } return { success: true, deletedFiles, freedSpace } } catch (error: any) { return { success: false, deletedFiles, freedSpace, error: error.message || '清理失败' } } } function formatBytes(bytes: number): string { if (bytes === 0) return '0 B' const k = 1024 const sizes = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i] } async function main() { console.log(`[Cleanup] 开始清理临时上传文件...`) console.log(`[Cleanup] 临时目录: ${path.join(UPLOAD_DIR, 'temp')}`) console.log(`[Cleanup] 最大年龄: ${MAX_AGE_HOURS} 小时`) const tempBaseDir = path.join(UPLOAD_DIR, 'temp') // 确保临时目录存在 try { await fs.access(tempBaseDir) } catch { console.log('[Cleanup] 临时目录不存在,无需清理') return } const now = Date.now() const tempDirs = await fs.readdir(tempBaseDir) let cleaned = 0 let totalFreedSpace = 0 const errors: string[] = [] for (const dir of tempDirs) { const uploadId = dir const tempDir = path.join(tempBaseDir, dir) try { // 检查目录 const stat = await fs.stat(tempDir) if (!stat.isDirectory()) { continue } // 读取元数据文件(如果存在) const metaPath = path.join(tempDir, 'meta.json') let createdAt = stat.mtimeMs // 使用目录修改时间作为fallback try { const metaContent = await fs.readFile(metaPath, 'utf-8') const meta = JSON.parse(metaContent) if (meta.createdAt) { createdAt = meta.createdAt } } catch { // 元数据文件不存在,使用目录修改时间 } // 检查是否过期 const age = now - createdAt const ageHours = age / (60 * 60 * 1000) const isExpired = age > MAX_AGE // 检查会话是否存在 let sessionExists = false try { const session = await getUploadSession(uploadId) sessionExists = !!session } catch (err) { // 获取会话失败,认为不存在 sessionExists = false } // 决定是否清理:过期的 或 会话不存在的 const shouldCleanup = isExpired || !sessionExists if (shouldCleanup) { const reason = isExpired ? `过期 (${ageHours.toFixed(1)}小时)` : '会话不存在' console.log(`[Cleanup] 清理上传会话: ${uploadId} (${reason})`) const result = await cleanupUploadSession(uploadId) if (result.success) { cleaned += result.deletedFiles totalFreedSpace += result.freedSpace console.log(`[Cleanup] ✓ 清理成功: ${result.deletedFiles} 个文件, 释放 ${formatBytes(result.freedSpace)}`) } else { errors.push(`${uploadId}: ${result.error}`) console.error(`[Cleanup] ✗ 清理失败: ${uploadId} - ${result.error}`) } } } catch (err: any) { errors.push(`${dir}: ${err.message || '读取失败'}`) console.error(`[Cleanup] ✗ 处理目录失败: ${dir} - ${err.message}`) } } console.log(`[Cleanup] 清理完成:`) console.log(` - 清理文件数: ${cleaned}`) console.log(` - 释放空间: ${formatBytes(totalFreedSpace)}`) if (errors.length > 0) { console.log(` - 错误数: ${errors.length}`) errors.forEach(err => console.log(` - ${err}`)) } } // 运行清理 main().catch(error => { console.error('[Cleanup] 清理过程出错:', error) process.exit(1) })