| import { logger } from '../logger'; |
| import fs from 'fs/promises'; |
| import path from 'path'; |
| import cron from 'node-cron'; |
| import { redis } from '../lib/redis'; |
|
|
| const TEMP_DIR = '/tmp'; |
| const GLOBAL_MAX_AGE = 30 * 60 * 1000; |
| const PER_TENANT_QUOTA_MB = 100; |
| const CLEANUP_LOCK_KEY = 'lock:cleanup:temp_files'; |
| const LOCK_TTL = 1800; |
|
|
| |
| |
| |
| |
| |
| |
| export async function cleanTempFiles() { |
| |
| const lock = await redis.set(CLEANUP_LOCK_KEY, 'locked', 'EX', LOCK_TTL, 'NX'); |
| if (!lock) { |
| logger.info('[CLEANUP] Maintenance already in progress on another instance. Skipping.'); |
| return; |
| } |
|
|
| const now = Date.now(); |
|
|
| try { |
| const folders = ['', 'audio', 'images', 'documents']; |
| const tenantUsage: Record<string, number> = {}; |
| const tenantFiles: Record<string, { path: string, size: number, mtime: number }[]> = {}; |
| |
| for (const folder of folders) { |
| const dirPath = path.join(TEMP_DIR, folder); |
| |
| try { |
| await fs.access(dirPath); |
| } catch { |
| continue; |
| } |
|
|
| const files = await fs.readdir(dirPath); |
| |
| for (const file of files) { |
| const filePath = path.join(dirPath, file); |
| const stats = await fs.stat(filePath); |
| |
| if (stats.isDirectory()) continue; |
|
|
| |
| const age = now - stats.mtimeMs; |
| if (age > GLOBAL_MAX_AGE) { |
| await fs.unlink(filePath); |
| logger.info(`[CLEANUP] Deleted old file (Age): ${filePath}`); |
| continue; |
| } |
|
|
| |
| const orgId = file.split('_')[0]; |
| if (orgId && orgId.length > 10) { |
| tenantUsage[orgId] = (tenantUsage[orgId] || 0) + stats.size; |
| if (!tenantFiles[orgId]) tenantFiles[orgId] = []; |
| tenantFiles[orgId].push({ path: filePath, size: stats.size, mtime: stats.mtimeMs }); |
| } |
| } |
| } |
|
|
| |
| for (const orgId in tenantUsage) { |
| const usageMB = tenantUsage[orgId] / (1024 * 1024); |
| if (usageMB > PER_TENANT_QUOTA_MB) { |
| logger.warn(`[CLEANUP] Quota exceeded for Org ${orgId}: ${usageMB.toFixed(2)}MB. Cleaning oldest files...`); |
| |
| |
| const files = tenantFiles[orgId].sort((a, b) => a.mtime - b.mtime); |
| let freed = 0; |
| const targetToFree = (usageMB - (PER_TENANT_QUOTA_MB * 0.8)) * 1024 * 1024; |
|
|
| for (const file of files) { |
| await fs.unlink(file.path); |
| freed += file.size; |
| if (freed >= targetToFree) break; |
| } |
| logger.info(`[CLEANUP] Freed ${(freed / (1024 * 1024)).toFixed(2)}MB for Org ${orgId}`); |
| } |
| } |
|
|
| } catch (err: any) { |
| logger.error({ err }, '[CLEANUP] Error during maintenance'); |
| } finally { |
| |
| await redis.del(CLEANUP_LOCK_KEY); |
| } |
| } |
|
|
| |
| |
| |
| export function startCleanupCron() { |
| cron.schedule('*/30 * * * *', async () => { |
| logger.info('[CLEANUP] 🧹 Starting scheduled maintenance...'); |
| await cleanTempFiles(); |
| }); |
| |
| |
| cleanTempFiles(); |
| } |
|
|