/** Audita trabajos interrumpidos. --limpiar retira solo huérfanos de más de 24 h. */ import { readdir, readFile, rm, stat } from 'node:fs/promises'; import { resolve, join, sep } from 'node:path'; import pg from 'pg'; const raices = [ resolve(process.env.MEDIA_DIR ?? 'media/audio'), resolve(process.env.ESCUCHA_DIR ?? 'media/escucha'), resolve(process.env.ONDA_DIR ?? 'static/ondas') ]; const limpiar = process.argv.includes('--limpiar'); const carpeta = join(raices[0], '.procesando'); const nombres = await readdir(carpeta).catch((f) => { if (f.code === 'ENOENT') return []; throw f; }); const cliente = new pg.Client({ connectionString: process.env.DATABASE_URL }); await cliente.connect(); try { for (const nombre of nombres.filter((n) => /^[a-f0-9-]{36}\.trabajo\.json$/.test(n))) { const ruta = join(carpeta, nombre), datos = JSON.parse(await readFile(ruta, 'utf8')); if ( !Array.isArray(datos.temporales) || !Array.isArray(datos.finales) || !Array.isArray(datos.claves) ) throw new Error('Manifiesto no válido'); const archivos = [...datos.temporales, ...datos.finales]; if ( archivos.some( (p) => typeof p !== 'string' || !raices.some((r) => resolve(p).startsWith(r + sep)) ) ) throw new Error('Manifiesto fuera del almacén'); const { rows } = await cliente.query('select clave from medio where clave=any($1::text[])', [ datos.claves ]); const antiguo = Date.now() - (await stat(ruta)).mtimeMs > 86400000; const estado = rows.length ? 'registrado: conservar archivos' : antiguo ? 'huérfano recuperable' : 'reciente: no tocar'; console.log(`${datos.slug}: ${estado}`); if (limpiar && antiguo) { // Una representación registrada basta para conservar TODOS los finales. for (const archivo of rows.length ? datos.temporales : archivos) await rm(archivo, { force: true }); await rm(ruta); } } } finally { await cliente.end(); }