#!/usr/bin/env node /** * Cleanup helper: * - Removes Photo docs whose main/variant paths are not WebP. * - Deletes non-WebP files in uploads. * - Optionally deletes orphaned WebP files (not referenced by any Photo) when DELETE_ORPHANS=1. * * Dry-run by default. Set APPLY=1 to make changes. */ const fs = require('fs'); const path = require('path'); const mongoose = require('mongoose'); const Photo = require('../models/photo'); const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/photogallery'; const APPLY = process.env.APPLY === '1'; const DELETE_ORPHANS = process.env.DELETE_ORPHANS === '1'; const UPLOAD_DIR = path.join(__dirname, '..', 'uploads'); const isWebp = (p) => /\.webp$/i.test(p || ''); async function main() { await mongoose.connect(MONGO_URI); console.log(`Connected to Mongo: ${MONGO_URI}`); // Find docs with non-webp main or variants const nonWebpDocs = await Photo.find({ $or: [ { path: { $not: /\.webp$/i } }, { 'variants.medium': { $exists: true, $not: /\.webp$/i } }, { 'variants.thumb': { $exists: true, $not: /\.webp$/i } }, ] }).select('_id path variants filename'); // Build referenced file set from remaining docs const allDocs = await Photo.find().select('path variants'); const referenced = new Set(); for (const doc of allDocs) { if (doc.path) referenced.add(doc.path); if (doc.variants?.medium) referenced.add(doc.variants.medium); if (doc.variants?.thumb) referenced.add(doc.variants.thumb); } // Scan uploads directory const filesOnDisk = []; const walk = (dir) => { for (const entry of fs.readdirSync(dir)) { const full = path.join(dir, entry); const stat = fs.statSync(full); if (stat.isDirectory()) walk(full); else filesOnDisk.push(full); } }; walk(UPLOAD_DIR); const nonWebpFiles = filesOnDisk.filter(f => !isWebp(f)); const orphans = filesOnDisk .filter(f => isWebp(f)) .filter(f => !referenced.has(path.relative(path.join(__dirname, '..'), f).replace(/\\/g, '/'))); console.log(`Found ${nonWebpDocs.length} photo docs with non-WebP paths/variants.`); console.log(`Found ${nonWebpFiles.length} non-WebP files on disk.`); console.log(`Found ${orphans.length} orphaned WebP files${DELETE_ORPHANS ? ' (will delete if APPLY=1)' : ''}.`); if (!APPLY) { console.log('Dry run (set APPLY=1 to apply changes).'); await mongoose.disconnect(); return; } if (nonWebpDocs.length) { const ids = nonWebpDocs.map(d => d._id); await Photo.deleteMany({ _id: { $in: ids } }); console.log(`Deleted ${ids.length} photo docs with non-WebP paths/variants.`); } for (const file of nonWebpFiles) { try { fs.unlinkSync(file); } catch (err) { console.error('Failed to delete', file, err.message); } } console.log(`Deleted ${nonWebpFiles.length} non-WebP files.`); if (DELETE_ORPHANS && orphans.length) { for (const file of orphans) { try { fs.unlinkSync(file); } catch (err) { console.error('Failed to delete orphan', file, err.message); } } console.log(`Deleted ${orphans.length} orphaned WebP files.`); } await mongoose.disconnect(); console.log('Cleanup complete.'); } main().catch(err => { console.error(err); process.exit(1); });