主要修复: - 修复批量上传闭包陷阱,防止文件丢失 - 添加 fileName 参数传递原始文件名,保留正确扩展名 - 添加取消检查回调,改进批量上传取消功能 其他改进: - 优化搜索功能和管理后台 - 添加 FRP 故障诊断文档 - 更新环境验证和网络检测
210 lines
5.4 KiB
TypeScript
210 lines
5.4 KiB
TypeScript
/**
|
||
* 临时文件自动清理模块
|
||
*
|
||
* 功能:
|
||
* 1. 应用启动时清理过期的 temp 目录
|
||
* 2. 定时清理(每小时检查一次)
|
||
* 3. 提供手动清理接口
|
||
*
|
||
* 清理规则:
|
||
* - 超过 8 小时的 temp 目录
|
||
* - 且在 Redis 中没有活跃会话
|
||
*/
|
||
|
||
import { promises as fs } from 'fs'
|
||
import path from 'path'
|
||
|
||
const UPLOAD_DIR = process.env.UPLOAD_DIR || './uploads'
|
||
const TEMP_DIR = path.join(UPLOAD_DIR, 'temp')
|
||
const MAX_AGE_HOURS = 8 // 8小时后清理
|
||
const MAX_AGE_MS = MAX_AGE_HOURS * 60 * 60 * 1000
|
||
const CLEANUP_INTERVAL_MS = 60 * 60 * 1000 // 每小时检查一次
|
||
|
||
let cleanupTimer: NodeJS.Timeout | null = null
|
||
let isCleanupRunning = false
|
||
|
||
/**
|
||
* 检查上传会话是否在 Redis 中活跃
|
||
*/
|
||
async function isSessionActive(uploadId: string): Promise<boolean> {
|
||
try {
|
||
const { hasUploadSession } = await import('./upload-sessions-redis')
|
||
return await hasUploadSession(uploadId)
|
||
} catch (error) {
|
||
// Redis 不可用时,保守处理,认为会话可能活跃
|
||
console.warn('[TempCleanup] 检查会话失败:', error)
|
||
return false
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理单个 temp 目录
|
||
*/
|
||
async function cleanupTempDir(dirPath: string): Promise<{ success: boolean; freedBytes: number }> {
|
||
let freedBytes = 0
|
||
|
||
try {
|
||
// 计算目录大小
|
||
const files = await fs.readdir(dirPath)
|
||
for (const file of files) {
|
||
try {
|
||
const filePath = path.join(dirPath, file)
|
||
const stat = await fs.stat(filePath)
|
||
if (stat.isFile()) {
|
||
freedBytes += stat.size
|
||
}
|
||
} catch {
|
||
// 忽略单个文件错误
|
||
}
|
||
}
|
||
|
||
// 删除目录及内容
|
||
await fs.rm(dirPath, { recursive: true, force: true })
|
||
return { success: true, freedBytes }
|
||
} catch (error) {
|
||
console.warn(`[TempCleanup] 删除目录失败: ${dirPath}`, error)
|
||
return { success: false, freedBytes: 0 }
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行清理任务
|
||
*/
|
||
export async function runCleanup(): Promise<{
|
||
cleaned: number
|
||
failed: number
|
||
freedBytes: number
|
||
skippedActive: number
|
||
}> {
|
||
if (isCleanupRunning) {
|
||
console.log('[TempCleanup] 清理任务正在运行,跳过')
|
||
return { cleaned: 0, failed: 0, freedBytes: 0, skippedActive: 0 }
|
||
}
|
||
|
||
isCleanupRunning = true
|
||
const startTime = Date.now()
|
||
|
||
let cleaned = 0
|
||
let failed = 0
|
||
let freedBytes = 0
|
||
let skippedActive = 0
|
||
|
||
try {
|
||
// 检查 temp 目录是否存在
|
||
try {
|
||
await fs.access(TEMP_DIR)
|
||
} catch {
|
||
console.log('[TempCleanup] temp 目录不存在,跳过清理')
|
||
return { cleaned, failed, freedBytes, skippedActive }
|
||
}
|
||
|
||
// 遍历 temp 目录
|
||
const dirs = await fs.readdir(TEMP_DIR)
|
||
const now = Date.now()
|
||
|
||
console.log(`[TempCleanup] 检查 ${dirs.length} 个目录...`)
|
||
|
||
for (const dir of dirs) {
|
||
const dirPath = path.join(TEMP_DIR, dir)
|
||
|
||
try {
|
||
const stat = await fs.stat(dirPath)
|
||
if (!stat.isDirectory()) continue
|
||
|
||
// 检查是否过期(使用目录修改时间)- 先检查时间,减少 Redis 查询
|
||
const age = now - stat.mtimeMs
|
||
if (age < MAX_AGE_MS) {
|
||
continue // 还没过期,跳过
|
||
}
|
||
|
||
// 检查是否是活跃会话(只对过期目录检查)
|
||
const isActive = await isSessionActive(dir)
|
||
if (isActive) {
|
||
skippedActive++
|
||
continue
|
||
}
|
||
|
||
// 执行清理
|
||
const result = await cleanupTempDir(dirPath)
|
||
if (result.success) {
|
||
cleaned++
|
||
freedBytes += result.freedBytes
|
||
} else {
|
||
failed++
|
||
}
|
||
} catch (error) {
|
||
failed++
|
||
console.warn(`[TempCleanup] 处理目录失败: ${dir}`, error)
|
||
}
|
||
}
|
||
|
||
const duration = Date.now() - startTime
|
||
const freedMB = (freedBytes / 1024 / 1024).toFixed(2)
|
||
console.log(`[TempCleanup] 清理完成: 删除 ${cleaned} 个目录,释放 ${freedMB} MB,跳过活跃 ${skippedActive} 个,耗时 ${duration}ms`)
|
||
|
||
} finally {
|
||
isCleanupRunning = false
|
||
}
|
||
|
||
return { cleaned, failed, freedBytes, skippedActive }
|
||
}
|
||
|
||
/**
|
||
* 启动定时清理任务
|
||
*/
|
||
export function startCleanupScheduler(): void {
|
||
if (cleanupTimer) {
|
||
console.log('[TempCleanup] 定时任务已在运行')
|
||
return
|
||
}
|
||
|
||
console.log('[TempCleanup] 启动定时清理任务(每小时执行)')
|
||
|
||
// 立即执行一次
|
||
runCleanup().catch(err => {
|
||
console.error('[TempCleanup] 启动时清理失败:', err)
|
||
})
|
||
|
||
// 设置定时任务
|
||
cleanupTimer = setInterval(() => {
|
||
runCleanup().catch(err => {
|
||
console.error('[TempCleanup] 定时清理失败:', err)
|
||
})
|
||
}, CLEANUP_INTERVAL_MS)
|
||
|
||
// 确保定时器不阻止进程退出
|
||
cleanupTimer.unref()
|
||
}
|
||
|
||
/**
|
||
* 停止定时清理任务
|
||
*/
|
||
export function stopCleanupScheduler(): void {
|
||
if (cleanupTimer) {
|
||
clearInterval(cleanupTimer)
|
||
cleanupTimer = null
|
||
console.log('[TempCleanup] 定时清理任务已停止')
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理指定的上传会话临时目录
|
||
* 用于上传完成或取消时立即清理
|
||
*/
|
||
export async function cleanupUploadSession(uploadId: string): Promise<boolean> {
|
||
const dirPath = path.join(TEMP_DIR, uploadId)
|
||
|
||
try {
|
||
await fs.access(dirPath)
|
||
} catch {
|
||
// 目录不存在,视为已清理
|
||
return true
|
||
}
|
||
|
||
const result = await cleanupTempDir(dirPath)
|
||
if (result.success) {
|
||
console.log(`[TempCleanup] 已清理上传会话: ${uploadId}`)
|
||
}
|
||
return result.success
|
||
}
|