Files
picpeak/backend/src/routes/adminArchives.js
T
paul 4e2075c638
Mirror to GitHub / mirror (push) Successful in 25s
Test and Lint / backend-test (push) Successful in 1m7s
continuous-integration/drone/push Build is passing
Test and Lint / frontend-test (push) Successful in 2m12s
Version and Release / version-bump (push) Successful in 33s
Version and Release / trigger-drone (push) Successful in 3s
fix: prevent double date formatting in email templates
- Remove pre-formatting of dates before passing to email processor
- Let email processor handle all date formatting based on recipient language
- Fix Invalid Date errors by passing raw date values instead of formatted strings
- Remove unused formatDate imports and variables

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-16 23:14:32 +02:00

411 lines
14 KiB
JavaScript

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;