182 lines
5.2 KiB
TypeScript
182 lines
5.2 KiB
TypeScript
/**
|
||
* 清理临时上传文件的定时任务脚本
|
||
* 用于清理: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)
|
||
})
|
||
|