From 1d94398e2d3444ddaaa2e1afe99a4b146014dbbe Mon Sep 17 00:00:00 2001 From: paul Date: Tue, 15 Jul 2025 11:32:36 +0200 Subject: [PATCH] fix: add thumbnail cleanup on archive deletion and diagnostic scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix archive deletion to also clean up associated thumbnails - Add cleanup-thumbnails.js script to remove temporary and orphaned thumbnails - Add diagnose-thumbnails.js script to troubleshoot thumbnail serving issues - Update README with documentation for new scripts This prevents thumbnail accumulation when events are deleted and helps diagnose why thumbnails might not be showing despite existing on disk. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- backend/scripts/README.md | 41 +++++++- backend/scripts/cleanup-thumbnails.js | 107 ++++++++++++++++++++ backend/scripts/diagnose-thumbnails.js | 132 +++++++++++++++++++++++++ backend/src/routes/adminArchives.js | 19 +++- 4 files changed, 297 insertions(+), 2 deletions(-) create mode 100755 backend/scripts/cleanup-thumbnails.js create mode 100755 backend/scripts/diagnose-thumbnails.js diff --git a/backend/scripts/README.md b/backend/scripts/README.md index 91af92b..d5fad50 100644 --- a/backend/scripts/README.md +++ b/backend/scripts/README.md @@ -61,4 +61,43 @@ On your production server: NODE_ENV=production node scripts/regenerate-thumbnails.js 2 ``` -Note: Replace `2` with the actual event ID from your database. \ No newline at end of file +Note: Replace `2` with the actual event ID from your database. + +## cleanup-thumbnails.js + +Cleans up temporary and orphaned thumbnail files. + +### Usage: +```bash +# Dry run - see what would be deleted +node scripts/cleanup-thumbnails.js --dry-run + +# Actually delete orphaned thumbnails +node scripts/cleanup-thumbnails.js +``` + +### What it does: +- Identifies temporary thumbnails (thumb_temp_*) +- Finds orphaned thumbnails not linked to any photo +- Removes unnecessary files to free up space +- Reports statistics on cleanup + +## diagnose-thumbnails.js + +Diagnoses why thumbnails might not be showing for a specific event. + +### Usage: +```bash +node scripts/diagnose-thumbnails.js 2 +``` + +### What it checks: +- Thumbnail paths in database vs filesystem +- Path format inconsistencies +- Missing thumbnail files +- Provides SQL to fix path issues + +### Common Issues: +1. **Path mismatch**: Database has wrong thumbnail path format +2. **Missing files**: Thumbnails were never generated +3. **Permission issues**: Web server can't read thumbnail files \ No newline at end of file diff --git a/backend/scripts/cleanup-thumbnails.js b/backend/scripts/cleanup-thumbnails.js new file mode 100755 index 0000000..e178966 --- /dev/null +++ b/backend/scripts/cleanup-thumbnails.js @@ -0,0 +1,107 @@ +#!/usr/bin/env node + +/** + * Script to clean up orphaned and temporary thumbnails + * Usage: node scripts/cleanup-thumbnails.js [--dry-run] + */ + +const path = require('path'); +const fs = require('fs').promises; +const { db } = require('../src/database/db'); + +const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, '../../storage'); +const THUMBNAILS_DIR = path.join(STORAGE_PATH, 'thumbnails'); + +async function cleanupThumbnails(dryRun = false) { + console.log('Starting thumbnail cleanup...'); + console.log(`Thumbnails directory: ${THUMBNAILS_DIR}`); + console.log(`Mode: ${dryRun ? 'DRY RUN' : 'LIVE'}\n`); + + try { + // Get all thumbnail files + const files = await fs.readdir(THUMBNAILS_DIR); + console.log(`Found ${files.length} files in thumbnails directory`); + + // Get all valid thumbnail paths from database + const validThumbnails = await db('photos') + .whereNotNull('thumbnail_path') + .select('thumbnail_path'); + + const validPaths = new Set( + validThumbnails.map(t => path.basename(t.thumbnail_path)) + ); + + console.log(`Found ${validPaths.size} valid thumbnails in database\n`); + + let tempCount = 0; + let orphanedCount = 0; + let validCount = 0; + let deletedCount = 0; + + for (const file of files) { + // Skip directories + const filePath = path.join(THUMBNAILS_DIR, file); + const stats = await fs.stat(filePath); + if (stats.isDirectory()) continue; + + // Check if it's a temporary file + if (file.startsWith('thumb_temp_')) { + tempCount++; + console.log(`Temporary file: ${file}`); + + if (!dryRun) { + try { + await fs.unlink(filePath); + deletedCount++; + } catch (error) { + console.error(` Failed to delete: ${error.message}`); + } + } + } + // Check if it's an orphaned thumbnail + else if (!validPaths.has(file)) { + orphanedCount++; + console.log(`Orphaned file: ${file}`); + + if (!dryRun) { + try { + await fs.unlink(filePath); + deletedCount++; + } catch (error) { + console.error(` Failed to delete: ${error.message}`); + } + } + } else { + validCount++; + } + } + + console.log('\n--- Summary ---'); + console.log(`Total files: ${files.length}`); + console.log(`Valid thumbnails: ${validCount}`); + console.log(`Temporary files: ${tempCount}`); + console.log(`Orphaned files: ${orphanedCount}`); + if (!dryRun) { + console.log(`Deleted files: ${deletedCount}`); + } else { + console.log(`Files to be deleted: ${tempCount + orphanedCount}`); + } + + } catch (error) { + console.error('Error during cleanup:', error); + process.exit(1); + } +} + +// Parse command line arguments +const dryRun = process.argv.includes('--dry-run'); + +// Run the cleanup +cleanupThumbnails(dryRun).then(async () => { + await db.destroy(); + console.log('\nCleanup complete'); +}).catch(async error => { + console.error('Cleanup failed:', error); + await db.destroy(); + process.exit(1); +}); \ No newline at end of file diff --git a/backend/scripts/diagnose-thumbnails.js b/backend/scripts/diagnose-thumbnails.js new file mode 100755 index 0000000..afc8f04 --- /dev/null +++ b/backend/scripts/diagnose-thumbnails.js @@ -0,0 +1,132 @@ +#!/usr/bin/env node + +/** + * Script to diagnose thumbnail serving issues + * Usage: node scripts/diagnose-thumbnails.js + */ + +const path = require('path'); +const fs = require('fs').promises; +const { db } = require('../src/database/db'); + +const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, '../../storage'); +const THUMBNAILS_DIR = path.join(STORAGE_PATH, 'thumbnails'); + +async function diagnoseThumbnails(eventId) { + if (!eventId) { + console.error('Usage: node scripts/diagnose-thumbnails.js '); + process.exit(1); + } + + console.log(`Diagnosing thumbnails for event ID: ${eventId}`); + console.log(`Storage path: ${STORAGE_PATH}`); + console.log(`Thumbnails directory: ${THUMBNAILS_DIR}\n`); + + try { + // Get event info + const event = await db('events').where('id', eventId).first(); + if (!event) { + console.error(`Event not found with ID: ${eventId}`); + return; + } + + console.log(`Event: ${event.event_name} (${event.slug})`); + console.log(`Active: ${event.is_active}, Archived: ${event.is_archived}\n`); + + // Get photos for this event + const photos = await db('photos') + .where('event_id', eventId) + .select('id', 'filename', 'path', 'thumbnail_path'); + + console.log(`Found ${photos.length} photos in database\n`); + + let missingThumbnails = 0; + let existingThumbnails = 0; + let pathIssues = []; + + for (const photo of photos.slice(0, 10)) { // Check first 10 photos + console.log(`Photo ID ${photo.id}: ${photo.filename}`); + console.log(` Photo path: ${photo.path}`); + console.log(` Thumbnail path in DB: ${photo.thumbnail_path}`); + + if (photo.thumbnail_path) { + // Expected thumbnail filename + const expectedThumbName = `thumb_${photo.filename}`; + const expectedThumbPath = path.join(THUMBNAILS_DIR, expectedThumbName); + + // Check if thumbnail exists + try { + await fs.access(expectedThumbPath); + console.log(` ✓ Thumbnail exists at: ${expectedThumbName}`); + existingThumbnails++; + + // Check if DB path matches expected path + const dbThumbName = path.basename(photo.thumbnail_path); + if (dbThumbName !== expectedThumbName) { + console.log(` ⚠ Path mismatch! DB has: ${dbThumbName}, Expected: ${expectedThumbName}`); + pathIssues.push({ + photoId: photo.id, + dbPath: photo.thumbnail_path, + expectedPath: `thumbnails/${expectedThumbName}` + }); + } + } catch { + console.log(` ✗ Thumbnail missing: ${expectedThumbName}`); + missingThumbnails++; + } + } else { + console.log(` ✗ No thumbnail path in database`); + missingThumbnails++; + } + console.log(''); + } + + console.log('--- Summary ---'); + console.log(`Existing thumbnails: ${existingThumbnails}`); + console.log(`Missing thumbnails: ${missingThumbnails}`); + console.log(`Path issues: ${pathIssues.length}`); + + if (pathIssues.length > 0) { + console.log('\n--- Path Issues ---'); + console.log('The following photos have incorrect thumbnail paths in the database:'); + for (const issue of pathIssues) { + console.log(`Photo ID ${issue.photoId}:`); + console.log(` Current: ${issue.dbPath}`); + console.log(` Should be: ${issue.expectedPath}`); + } + + console.log('\nTo fix path issues, run:'); + console.log(`UPDATE photos SET thumbnail_path = 'thumbnails/thumb_' || filename WHERE event_id = ${eventId};`); + } + + // Check for any thumbnails in the directory that match this event + const files = await fs.readdir(THUMBNAILS_DIR); + const eventThumbnails = files.filter(f => { + // Try to match thumbnails for this event + for (const photo of photos) { + if (f === `thumb_${photo.filename}`) return true; + } + return false; + }); + + console.log(`\n--- Filesystem Check ---`); + console.log(`Found ${eventThumbnails.length} thumbnails in directory for this event`); + + } catch (error) { + console.error('Error during diagnosis:', error); + process.exit(1); + } +} + +// Parse command line arguments +const eventId = process.argv[2] ? parseInt(process.argv[2]) : null; + +// Run the diagnosis +diagnoseThumbnails(eventId).then(async () => { + await db.destroy(); + console.log('\nDiagnosis complete'); +}).catch(async error => { + console.error('Diagnosis failed:', error); + await db.destroy(); + process.exit(1); +}); \ No newline at end of file diff --git a/backend/src/routes/adminArchives.js b/backend/src/routes/adminArchives.js index a052d1c..f2102d2 100644 --- a/backend/src/routes/adminArchives.js +++ b/backend/src/routes/adminArchives.js @@ -363,12 +363,29 @@ router.delete('/:id', adminAuth, async (req, res) => { // Delete archive file if exists if (archive.archive_path) { try { - await fs.unlink(archive.archive_path); + const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); + const fullArchivePath = path.join(storagePath, archive.archive_path); + await fs.unlink(fullArchivePath); } catch (error) { console.error('Failed to delete archive file:', error); } } + // Delete thumbnails for this event + const photos = await db('photos').where('event_id', req.params.id).select('thumbnail_path'); + const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); + + for (const photo of photos) { + if (photo.thumbnail_path) { + try { + const thumbPath = path.join(storagePath, photo.thumbnail_path.replace(/^\//, '')); + await fs.unlink(thumbPath); + } catch (error) { + // Ignore errors - thumbnail might already be deleted + } + } + } + // Delete from database (cascade will delete photos and logs) await db('events').where('id', req.params.id).delete();