jedmund-svelte/scripts/cloudinary-cleanup.ts
Justin Edmund 1f04a96dad feat: add Cloudinary audit functionality
- Add comprehensive audit system to identify orphaned Cloudinary files
- Create audit script with dry-run and execute modes
- Add formatBytes utility for human-readable file sizes
- Implement comparison logic between Cloudinary and database references
- Add API endpoint for programmatic access to audit functionality
- Include documentation for Cloudinary management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-16 16:56:05 +01:00

151 lines
4.3 KiB
TypeScript

#!/usr/bin/env tsx
import { config } from 'dotenv'
import { v2 as cloudinary } from 'cloudinary'
import {
auditCloudinaryResources,
deleteOrphanedFiles,
type AuditResult
} from '../src/lib/server/cloudinary-audit'
import { formatBytes } from '../src/lib/utils/format'
// Load environment variables
config()
// Configure Cloudinary
cloudinary.config({
cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
api_key: process.env.CLOUDINARY_API_KEY,
api_secret: process.env.CLOUDINARY_API_SECRET
})
/**
* Main cleanup script
*/
async function main() {
const args = process.argv.slice(2)
const isDryRun = !args.includes('--execute')
const verbose = args.includes('--verbose')
console.log('🔍 Cloudinary Cleanup Script')
console.log('===========================')
console.log(`Mode: ${isDryRun ? 'DRY RUN' : 'EXECUTE'}`)
console.log('')
try {
// Run audit
console.log('📊 Running audit...')
const audit = await auditCloudinaryResources()
// Display results
displayAuditResults(audit, verbose)
// Handle cleanup if orphaned files exist
if (audit.orphanedFiles.length > 0) {
console.log('')
if (isDryRun) {
console.log('⚠️ DRY RUN MODE: No files will be deleted')
console.log(' Run with --execute flag to delete orphaned files')
} else {
console.log('🗑️ Preparing to delete orphaned files...')
const confirm = await promptConfirmation(
`Delete ${audit.orphanedFiles.length} orphaned files (${formatBytes(audit.orphanedTotalBytes)})?`
)
if (confirm) {
const publicIds = audit.orphanedFiles.map((f) => f.public_id)
const deleteResults = await deleteOrphanedFiles(publicIds, false)
console.log('')
console.log('✅ Deletion Results:')
console.log(` Attempted: ${deleteResults.attempted}`)
console.log(` Succeeded: ${deleteResults.succeeded}`)
console.log(` Failed: ${deleteResults.failed.length}`)
if (deleteResults.failed.length > 0 && verbose) {
console.log('')
console.log('❌ Failed deletions:')
deleteResults.failed.forEach((id) => console.log(` - ${id}`))
}
} else {
console.log('❌ Cleanup cancelled')
}
}
} else {
console.log('')
console.log('✅ No orphaned files found! Your Cloudinary storage is clean.')
}
// Handle missing files
if (audit.missingFromCloudinary.length > 0) {
console.log('')
console.log('⚠️ Warning: Database references files missing from Cloudinary')
console.log(` Found ${audit.missingFromCloudinary.length} missing references`)
console.log(' Consider cleaning up these database entries')
if (verbose) {
console.log('')
console.log('Missing public IDs:')
audit.missingFromCloudinary.forEach((id) => console.log(` - ${id}`))
}
}
} catch (error) {
console.error('❌ Error:', error)
process.exit(1)
}
}
/**
* Display audit results in a formatted way
*/
function displayAuditResults(audit: AuditResult, verbose: boolean) {
console.log('')
console.log('📈 Audit Summary:')
console.log(` Total files in Cloudinary: ${audit.totalCloudinaryFiles}`)
console.log(` Total database references: ${audit.totalDatabaseReferences}`)
console.log(` Orphaned files: ${audit.orphanedFiles.length}`)
console.log(` Orphaned storage size: ${formatBytes(audit.orphanedTotalBytes)}`)
console.log(` Missing from Cloudinary: ${audit.missingFromCloudinary.length}`)
if (verbose && audit.orphanedFiles.length > 0) {
console.log('')
console.log('📁 Orphaned Files:')
// Group by folder
const byFolder = audit.orphanedFiles.reduce(
(acc, file) => {
const folder = file.folder || 'root'
if (!acc[folder]) acc[folder] = []
acc[folder].push(file)
return acc
},
{} as Record<string, typeof audit.orphanedFiles>
)
Object.entries(byFolder).forEach(([folder, files]) => {
console.log(` 📂 ${folder}/ (${files.length} files)`)
files.forEach((file) => {
console.log(` - ${file.public_id} (${formatBytes(file.bytes)})`)
})
})
}
}
/**
* Prompt for user confirmation
*/
async function promptConfirmation(message: string): Promise<boolean> {
console.log('')
console.log(`${message} (y/N): `)
return new Promise((resolve) => {
process.stdin.once('data', (data) => {
const answer = data.toString().trim().toLowerCase()
resolve(answer === 'y' || answer === 'yes')
})
})
}
// Run the script
main().catch(console.error)