ManimCat-show / src /services /media-cleanup.ts
Bin29's picture
版本为1.3
32aacff
import { promises as fs, type Dirent } from 'fs'
import path from 'path'
import { createLogger } from '../utils/logger'
const logger = createLogger('MediaCleanup')
const DEFAULT_RETENTION_HOURS = 72
const DEFAULT_CLEANUP_INTERVAL_MINUTES = 60
interface CleanupSummary {
removedFiles: number
freedBytes: number
}
function parsePositiveInteger(input: string | undefined, fallback: number): number {
const value = Number(input)
if (!Number.isFinite(value) || value <= 0) {
return fallback
}
return Math.floor(value)
}
function getRetentionMs(): number {
const hours = parsePositiveInteger(process.env.MEDIA_RETENTION_HOURS, DEFAULT_RETENTION_HOURS)
return hours * 60 * 60 * 1000
}
function getCleanupIntervalMs(): number {
const minutes = parsePositiveInteger(
process.env.MEDIA_CLEANUP_INTERVAL_MINUTES,
DEFAULT_CLEANUP_INTERVAL_MINUTES
)
return minutes * 60 * 1000
}
async function cleanupDirectory(
dir: string,
cutoffTime: number,
extensions: string[]
): Promise<CleanupSummary> {
let removedFiles = 0
let freedBytes = 0
let entries: Dirent[]
try {
entries = await fs.readdir(dir, { withFileTypes: true })
} catch {
return { removedFiles, freedBytes }
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name)
if (entry.isDirectory()) {
const nested = await cleanupDirectory(fullPath, cutoffTime, extensions)
removedFiles += nested.removedFiles
freedBytes += nested.freedBytes
continue
}
if (!extensions.some((ext) => entry.name.toLowerCase().endsWith(ext))) {
continue
}
try {
const stat = await fs.stat(fullPath)
if (stat.mtimeMs > cutoffTime) {
continue
}
await fs.unlink(fullPath)
removedFiles += 1
freedBytes += stat.size
} catch (error) {
logger.warn('删除媒体文件失败', { file: fullPath, error: String(error) })
}
}
return { removedFiles, freedBytes }
}
export async function cleanupExpiredMediaFiles(): Promise<{
images: CleanupSummary
videos: CleanupSummary
}> {
const retentionMs = getRetentionMs()
const cutoffTime = Date.now() - retentionMs
const imagesDir = path.join(process.cwd(), 'public', 'images')
const videosDir = path.join(process.cwd(), 'public', 'videos')
const [images, videos] = await Promise.all([
cleanupDirectory(imagesDir, cutoffTime, ['.png', '.jpg', '.jpeg', '.webp']),
cleanupDirectory(videosDir, cutoffTime, ['.mp4'])
])
if (images.removedFiles > 0 || videos.removedFiles > 0) {
logger.info('媒体文件清理完成', {
retentionHours: retentionMs / 1000 / 60 / 60,
imagesRemoved: images.removedFiles,
videosRemoved: videos.removedFiles,
freedMB: Math.round((images.freedBytes + videos.freedBytes) / 1024 / 1024 * 100) / 100
})
}
return { images, videos }
}
export function startMediaCleanupScheduler(): () => void {
const intervalMs = getCleanupIntervalMs()
cleanupExpiredMediaFiles().catch((error) => {
logger.warn('启动时媒体清理失败', { error: String(error) })
})
const timer = setInterval(() => {
cleanupExpiredMediaFiles().catch((error) => {
logger.warn('周期媒体清理失败', { error: String(error) })
})
}, intervalMs)
timer.unref()
logger.info('媒体清理调度器已启动', {
intervalMinutes: Math.round(intervalMs / 1000 / 60),
retentionHours: Math.round(getRetentionMs() / 1000 / 60 / 60)
})
return () => clearInterval(timer)
}