soroban-abacus-flashcards/apps/web/scripts/cleanupVisionRecordings.ts

117 lines
3.1 KiB
TypeScript

#!/usr/bin/env npx tsx
/**
* Cleanup script for expired vision recordings
*
* Deletes recordings that have passed their expiresAt timestamp.
* Should be run periodically via cron job (e.g., daily).
*
* Usage:
* npx tsx scripts/cleanupVisionRecordings.ts
*
* Options:
* --dry-run Show what would be deleted without actually deleting
* --verbose Show detailed output
*/
import { eq, lt } from 'drizzle-orm'
import { rm } from 'fs/promises'
import path from 'path'
import { db } from '../src/db'
import { visionRecordings } from '../src/db/schema/vision-recordings'
async function main() {
const args = process.argv.slice(2)
const dryRun = args.includes('--dry-run')
const verbose = args.includes('--verbose')
console.log('='.repeat(60))
console.log('Vision Recording Cleanup')
console.log('='.repeat(60))
console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}`)
console.log(`Time: ${new Date().toISOString()}`)
console.log('')
const now = new Date()
// Find expired recordings
const expiredRecordings = await db.query.visionRecordings.findMany({
where: lt(visionRecordings.expiresAt, now),
})
console.log(`Found ${expiredRecordings.length} expired recording(s)`)
if (expiredRecordings.length === 0) {
console.log('Nothing to clean up.')
return
}
let deletedCount = 0
let errorCount = 0
let totalSizeBytes = 0
for (const recording of expiredRecordings) {
const recordingDir = path.join(
process.cwd(),
'data',
'uploads',
'vision-recordings',
recording.playerId,
recording.id
)
if (verbose) {
console.log(`\nProcessing: ${recording.id}`)
console.log(` Session: ${recording.sessionId}`)
console.log(` Player: ${recording.playerId}`)
console.log(` Expired: ${recording.expiresAt.toISOString()}`)
console.log(
` Size: ${recording.fileSize ? `${(recording.fileSize / 1024 / 1024).toFixed(2)} MB` : 'unknown'}`
)
console.log(` Path: ${recordingDir}`)
}
if (recording.fileSize) {
totalSizeBytes += recording.fileSize
}
if (dryRun) {
console.log(`[DRY RUN] Would delete: ${recording.id}`)
deletedCount++
continue
}
try {
// Delete files from disk
await rm(recordingDir, { recursive: true, force: true })
// Delete database record
await db.delete(visionRecordings).where(eq(visionRecordings.id, recording.id))
deletedCount++
console.log(`Deleted: ${recording.id}`)
} catch (error) {
errorCount++
console.error(`Error deleting ${recording.id}:`, error)
}
}
console.log('')
console.log('-'.repeat(60))
console.log('Summary:')
console.log(` Total expired: ${expiredRecordings.length}`)
console.log(` Deleted: ${deletedCount}`)
console.log(` Errors: ${errorCount}`)
console.log(` Space recovered: ${(totalSizeBytes / 1024 / 1024).toFixed(2)} MB`)
console.log('')
if (dryRun) {
console.log('NOTE: This was a dry run. No files were actually deleted.')
console.log('Run without --dry-run to perform actual cleanup.')
}
}
main().catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
})