gongxiangp2/lib/temp-cleanup.ts
chenpei 752b7bf999 fix: 修复批量上传和视频扩展名问题
主要修复:

- 修复批量上传闭包陷阱,防止文件丢失

- 添加 fileName 参数传递原始文件名,保留正确扩展名

- 添加取消检查回调,改进批量上传取消功能

其他改进:

- 优化搜索功能和管理后台

- 添加 FRP 故障诊断文档

- 更新环境验证和网络检测
2026-02-03 11:12:50 +08:00

210 lines
5.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 临时文件自动清理模块
*
* 功能:
* 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
}