#!/usr/bin/env node /** * Relink variants for existing Photo docs based on files in uploads/. * For each Photo doc, derive baseName from its filename and fill in: * - path -> uploads/-main (or first available in group) * - variants.medium / variants.thumb -> matching files if present * * Dry run by default; set APPLY=1 to save. */ 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 UPLOAD_DIR = path.join(__dirname, '..', 'uploads'); const VARIANT_KEYS = { '': 'main', '-md': 'medium', '-sm': 'thumb' }; function parseFile(file) { const ext = path.extname(file); if (ext.toLowerCase() !== '.webp') return null; const base = path.basename(file, ext); const match = base.match(/^(.*?)(-md|-sm)?$/); if (!match) return null; return { baseName: match[1], variantKey: VARIANT_KEYS[match[2] || ''], filename: file }; } function scanUploads() { const groups = {}; const files = fs.readdirSync(UPLOAD_DIR).filter(f => f.toLowerCase().endsWith('.webp')); for (const file of files) { const parsed = parseFile(file); if (!parsed) continue; const { baseName, variantKey, filename } = parsed; if (!groups[baseName]) groups[baseName] = {}; groups[baseName][variantKey] = filename; } return groups; } async function main() { await mongoose.connect(MONGO_URI); console.log(`Connected to Mongo: ${MONGO_URI}`); const groups = scanUploads(); let updated = 0; const missingGroups = []; const docs = await Photo.find({}); // Index docs by filename/path and baseName to improve matching const byFilename = new Map(); const byPath = new Map(); const byBase = new Map(); // baseName -> array of docs for (const doc of docs) { const fname = doc.filename || path.basename(doc.path || ''); byFilename.set(fname, doc); if (doc.path) { const rel = doc.path.replace(/\\/g, '/'); byPath.set(rel.startsWith('uploads/') ? rel.slice(''.length) : rel, doc); byPath.set(rel, doc); } const parsed = parseFile(fname); if (parsed) { const arr = byBase.get(parsed.baseName) || []; arr.push(doc); byBase.set(parsed.baseName, arr); } } for (const [baseName, variants] of Object.entries(groups)) { const mainFile = variants.main || Object.values(variants)[0]; const relMain = path.join('uploads', mainFile).replace(/\\/g, '/'); let doc = byFilename.get(mainFile) || byPath.get(relMain) || (() => { const arr = byBase.get(baseName) || []; return arr.length === 1 ? arr[0] : null; })(); if (!doc) { missingGroups.push(baseName); continue; } const newPath = relMain; const newVariants = { medium: variants.medium ? path.join('uploads', variants.medium).replace(/\\/g, '/') : undefined, thumb: variants.thumb ? path.join('uploads', variants.thumb).replace(/\\/g, '/') : undefined, }; const pathChanged = doc.path !== newPath; const medChanged = (doc.variants?.medium || undefined) !== newVariants.medium; const thumbChanged = (doc.variants?.thumb || undefined) !== newVariants.thumb; if (pathChanged || medChanged || thumbChanged) { if (APPLY) { doc.path = newPath; doc.variants = newVariants; await doc.save(); } updated++; } } console.log(`Relinked variants for ${updated} photos.${APPLY ? '' : ' (dry run, no writes)'}`); if (missingGroups.length) { const sample = missingGroups.slice(0, 10).join(', '); console.log(`Skipped ${missingGroups.length} upload groups with no matching Photo doc. Examples: ${sample}`); } await mongoose.disconnect(); } main().catch(err => { console.error(err); process.exit(1); });