const express = require('express'); const path = require('path'); const fs = require('fs').promises; const { db } = require('../database/db'); const { formatBoolean } = require('../utils/dbCompat'); const { adminAuth } = require('../middleware/auth-enhanced-v2'); const archiver = require('archiver'); const AdmZip = require('adm-zip'); const router = express.Router(); // Get all archived events router.get('/', adminAuth, async (req, res) => { try { const page = parseInt(req.query.page) || 1; const limit = parseInt(req.query.limit) || 20; const offset = (page - 1) * limit; // Get total count const totalCount = await db('events') .where('is_archived', formatBoolean(true)) .count('id as count') .first(); // Get archived events const archives = await db('events') .select( 'events.*', db.raw('COUNT(DISTINCT photos.id) as photo_count'), db.raw('SUM(photos.size_bytes) as total_size') ) .leftJoin('photos', 'events.id', 'photos.event_id') .where('events.is_archived', formatBoolean(true)) .groupBy('events.id') .orderBy('events.archived_at', 'desc') .limit(limit) .offset(offset); // Check if archive files exist and get their sizes const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); const archivesWithFileInfo = await Promise.all(archives.map(async (archive) => { let archiveFileSize = 0; if (archive.archive_path) { try { const fullArchivePath = path.join(storagePath, archive.archive_path); const stats = await fs.stat(fullArchivePath); archiveFileSize = stats.size; } catch (error) { console.error(`Archive file not found: ${archive.archive_path}`); } } return { id: archive.id, slug: archive.slug, eventName: archive.event_name, eventDate: archive.event_date, eventType: archive.event_type, hostEmail: archive.host_email, archivedAt: archive.archived_at ? new Date(archive.archived_at).toISOString() : null, expiresAt: archive.expires_at ? new Date(archive.expires_at).toISOString() : null, photoCount: archive.photo_count || 0, originalSize: archive.total_size || 0, archiveSize: archiveFileSize, archivePath: archive.archive_path }; })); res.json({ archives: archivesWithFileInfo, pagination: { page, limit, total: totalCount.count, totalPages: Math.ceil(totalCount.count / limit) } }); } catch (error) { console.error('Archives list error:', error); res.status(500).json({ error: 'Failed to fetch archives' }); } }); // Get single archive details router.get('/:id', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) .where('is_archived', formatBoolean(true)) .first(); if (!archive) { return res.status(404).json({ error: 'Archive not found' }); } // Get photo details const photos = await db('photos') .where('event_id', archive.id) .select('filename', 'type', 'size_bytes', 'uploaded_at'); // Check archive file let archiveFileInfo = null; if (archive.archive_path) { try { const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); const fullArchivePath = path.join(storagePath, archive.archive_path); const stats = await fs.stat(fullArchivePath); archiveFileInfo = { size: stats.size, createdAt: stats.birthtime, path: archive.archive_path }; } catch (error) { console.error('Archive file not found:', error); } } res.json({ id: archive.id, slug: archive.slug, eventName: archive.event_name, eventDate: archive.event_date, eventType: archive.event_type, hostEmail: archive.host_email, adminEmail: archive.admin_email, welcomeMessage: archive.welcome_message, colorTheme: archive.color_theme, createdAt: archive.created_at, expiresAt: archive.expires_at, archivedAt: archive.archived_at, photos: photos, archiveFile: archiveFileInfo }); } catch (error) { console.error('Archive details error:', error); res.status(500).json({ error: 'Failed to fetch archive details' }); } }); // Restore archive router.post('/:id/restore', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) .where('is_archived', formatBoolean(true)) .first(); if (!archive) { return res.status(404).json({ error: 'Archive not found' }); } // Check if archive file exists if (!archive.archive_path) { return res.status(400).json({ error: 'No archive file found' }); } const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); const fullArchivePath = path.join(storagePath, archive.archive_path); try { await fs.access(fullArchivePath); } catch (error) { return res.status(404).json({ error: 'Archive file not found on disk' }); } // Extract the archive try { const zip = new AdmZip(fullArchivePath); const eventsDir = path.join(storagePath, 'events/active'); const eventDir = path.join(eventsDir, archive.slug); // Create event directory if it doesn't exist await fs.mkdir(eventDir, { recursive: true }); // Log ZIP contents for debugging console.log(`Extracting archive to: ${eventDir}`); const entries = zip.getEntries(); console.log(`Archive contains ${entries.length} entries`); // Extract files to the event directory zip.extractAllTo(eventDir, true); // Get list of extracted files to update database const extractedPhotos = []; // First, collect all category information from the ZIP structure const categoriesMap = new Map(); for (const entry of entries) { if (!entry.isDirectory && entry.entryName.match(/\.(jpg|jpeg|png|gif|webp)$/i)) { const filename = path.basename(entry.entryName); const dirPath = path.dirname(entry.entryName); const actualFilePath = path.join(eventDir, entry.entryName); try { // Check if file was extracted successfully const stats = await fs.stat(actualFilePath); // Determine category from directory structure let categoryId = null; if (dirPath && dirPath !== '.') { // Get the first level directory as category const categoryName = dirPath.split(path.sep)[0]; if (!categoriesMap.has(categoryName)) { // Check if this category exists in the database const existingCategory = await db('photo_categories') .where('event_id', archive.id) .where('name', categoryName) .first(); if (existingCategory) { categoriesMap.set(categoryName, existingCategory.id); } else { // Create the category if it doesn't exist const insertResult = await db('photo_categories').insert({ event_id: archive.id, name: categoryName, slug: categoryName.toLowerCase().replace(/[^a-z0-9]/g, '-'), created_at: new Date() }).returning('id'); const newCategoryId = insertResult[0]?.id || insertResult[0]; categoriesMap.set(categoryName, newCategoryId); } } categoryId = categoriesMap.get(categoryName); } // Check if photo already exists in database const existingPhoto = await db('photos') .where('event_id', archive.id) .where('filename', filename) .first(); if (!existingPhoto) { // Store relative path from storage root const relativePath = path.relative(storagePath, actualFilePath); extractedPhotos.push({ event_id: archive.id, filename: filename, original_filename: filename, path: relativePath, thumbnail_path: null, // Will be regenerated by thumbnail service type: path.extname(filename).substring(1).toLowerCase(), size_bytes: stats.size, category_id: categoryId, uploaded_at: new Date() }); } } catch (statError) { console.error(`Failed to stat file: ${actualFilePath}`); console.error(`Entry name was: ${entry.entryName}`); console.error('Error:', statError.message); // Skip this file if we can't stat it continue; } } } // Insert new photos if any if (extractedPhotos.length > 0) { await db('photos').insert(extractedPhotos); } } catch (extractError) { console.error('Archive extraction error:', extractError); return res.status(500).json({ error: 'Failed to extract archive: ' + extractError.message }); } // Update event status const thirtyDaysFromNow = new Date(); thirtyDaysFromNow.setDate(thirtyDaysFromNow.getDate() + 30); await db('events') .where('id', req.params.id) .update({ is_archived: false, is_active: true, archive_path: null, archived_at: null, expires_at: thirtyDaysFromNow.toISOString() // Reset expiration - works on both DBs }); // Log activity await db('activity_logs').insert({ activity_type: 'archive_restored', actor_type: 'admin', actor_id: req.admin.id, actor_name: req.admin.username, event_id: archive.id, metadata: JSON.stringify({ event_name: archive.event_name }) }); res.json({ message: 'Archive restored successfully' }); } catch (error) { console.error('Archive restore error:', error); res.status(500).json({ error: 'Failed to restore archive' }); } }); // Download archive router.get('/:id/download', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) .where('is_archived', formatBoolean(true)) .first(); if (!archive) { return res.status(404).json({ error: 'Archive not found' }); } if (!archive.archive_path) { return res.status(404).json({ error: 'Archive file not found' }); } // Check if file exists const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage'); const fullArchivePath = path.join(storagePath, archive.archive_path); try { await fs.access(fullArchivePath); } catch (error) { return res.status(404).json({ error: 'Archive file not found on disk' }); } // Set headers for download res.setHeader('Content-Type', 'application/zip'); res.setHeader('Content-Disposition', `attachment; filename="${archive.slug}.zip"`); // Stream the file const fileStream = require('fs').createReadStream(fullArchivePath); fileStream.pipe(res); // Log download await db('activity_logs').insert({ activity_type: 'archive_downloaded', actor_type: 'admin', actor_id: req.admin.id, actor_name: req.admin.username, event_id: archive.id, metadata: JSON.stringify({ event_name: archive.event_name }) }); } catch (error) { console.error('Archive download error:', error); res.status(500).json({ error: 'Failed to download archive' }); } }); // Delete archive permanently router.delete('/:id', adminAuth, async (req, res) => { try { const archive = await db('events') .where('id', req.params.id) .where('is_archived', formatBoolean(true)) .first(); if (!archive) { return res.status(404).json({ error: 'Archive not found' }); } // Delete archive file if exists if (archive.archive_path) { try { 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(); // Log activity await db('activity_logs').insert({ activity_type: 'archive_deleted', actor_type: 'admin', actor_id: req.admin.id, actor_name: req.admin.username, metadata: JSON.stringify({ event_name: archive.event_name, archived_date: archive.archived_at }) }); res.json({ message: 'Archive deleted permanently' }); } catch (error) { console.error('Archive delete error:', error); res.status(500).json({ error: 'Failed to delete archive' }); } }); module.exports = router;