- Add try-catch and file existence check for photo path resolution (#161) - Fix gallery categories to use photo_categories table instead of legacy type field (#156) - Add byte-size-based chunking (500MB max) for uploads to prevent oversized batches (#155)
This commit is contained in:
@@ -294,20 +294,35 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
|||||||
commentMap[c.photo_id] = parseInt(c.comment_count);
|
commentMap[c.photo_id] = parseInt(c.comment_count);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get distinct photo types for this event
|
// Get actual categories used by photos in this event
|
||||||
const categoryResults = await db('photos')
|
// This includes both global categories and event-specific ones
|
||||||
|
const usedCategoryIds = await db('photos')
|
||||||
.where('event_id', req.event.id)
|
.where('event_id', req.event.id)
|
||||||
.select('type')
|
.whereNotNull('category_id')
|
||||||
.distinct('type')
|
.distinct('category_id')
|
||||||
.orderBy('type', 'asc');
|
.pluck('category_id');
|
||||||
|
|
||||||
// Convert types to category-like objects
|
// Fetch category details from photo_categories table
|
||||||
const categories = categoryResults.map(result => ({
|
let categories = [];
|
||||||
id: result.type,
|
if (usedCategoryIds.length > 0) {
|
||||||
name: result.type === 'individual' ? 'Individual Photos' : 'Collages',
|
const categoryDetails = await db('photo_categories')
|
||||||
slug: result.type,
|
.whereIn('id', usedCategoryIds)
|
||||||
is_global: false
|
.select('id', 'name', 'slug', 'is_global')
|
||||||
|
.orderBy('name', 'asc');
|
||||||
|
|
||||||
|
categories = categoryDetails.map(cat => ({
|
||||||
|
id: cat.id,
|
||||||
|
name: cat.name,
|
||||||
|
slug: cat.slug,
|
||||||
|
is_global: cat.is_global
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map for quick category lookup
|
||||||
|
const categoryMap = {};
|
||||||
|
categories.forEach(cat => {
|
||||||
|
categoryMap[cat.id] = cat;
|
||||||
|
});
|
||||||
|
|
||||||
// Log view
|
// Log view
|
||||||
await db('access_logs').insert({
|
await db('access_logs').insert({
|
||||||
@@ -369,9 +384,9 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
|||||||
secure_url_template: `/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`,
|
secure_url_template: `/api/secure-images/${req.params.slug}/secure/${photo.id}/{{token}}`,
|
||||||
download_url_template: `/api/secure-images/${req.params.slug}/secure-download/${photo.id}/{{token}}`,
|
download_url_template: `/api/secure-images/${req.params.slug}/secure-download/${photo.id}/{{token}}`,
|
||||||
type: photo.type,
|
type: photo.type,
|
||||||
category_id: photo.type,
|
category_id: photo.category_id || null,
|
||||||
category_name: photo.type === 'individual' ? 'Individual Photos' : 'Collages',
|
category_name: photo.category_id && categoryMap[photo.category_id] ? categoryMap[photo.category_id].name : null,
|
||||||
category_slug: photo.type,
|
category_slug: photo.category_id && categoryMap[photo.category_id] ? categoryMap[photo.category_id].slug : null,
|
||||||
size: photo.size_bytes,
|
size: photo.size_bytes,
|
||||||
uploaded_at: photo.uploaded_at,
|
uploaded_at: photo.uploaded_at,
|
||||||
// Image dimensions for layout calculations
|
// Image dimensions for layout calculations
|
||||||
@@ -732,8 +747,34 @@ router.get('/:slug/photo/:photoId',
|
|||||||
|
|
||||||
// Resolve the absolute file path for this photo, supporting both managed and external reference modes
|
// Resolve the absolute file path for this photo, supporting both managed and external reference modes
|
||||||
const { resolvePhotoFilePath } = require('../services/photoResolver');
|
const { resolvePhotoFilePath } = require('../services/photoResolver');
|
||||||
const filePath = resolvePhotoFilePath(req.event, photo);
|
const fs = require('fs');
|
||||||
|
|
||||||
|
let filePath;
|
||||||
|
try {
|
||||||
|
filePath = resolvePhotoFilePath(req.event, photo);
|
||||||
|
} catch (resolveError) {
|
||||||
|
logger.error('Failed to resolve photo path', {
|
||||||
|
slug: req.params.slug,
|
||||||
|
photoId,
|
||||||
|
eventId: req.event.id,
|
||||||
|
error: resolveError.message,
|
||||||
|
photoPath: photo.path,
|
||||||
|
photoFilename: photo.filename
|
||||||
|
});
|
||||||
|
return res.status(404).json({ error: 'Photo file not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify file exists before attempting to serve
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
logger.error('Photo file does not exist at resolved path', {
|
||||||
|
slug: req.params.slug,
|
||||||
|
photoId,
|
||||||
|
eventId: req.event.id,
|
||||||
|
resolvedPath: filePath,
|
||||||
|
photoPath: photo.path
|
||||||
|
});
|
||||||
|
return res.status(404).json({ error: 'Photo file not found' });
|
||||||
|
}
|
||||||
|
|
||||||
// Log access - temporarily disabled for debugging
|
// Log access - temporarily disabled for debugging
|
||||||
// await secureImageService.logImageAccess(
|
// await secureImageService.logImageAccess(
|
||||||
@@ -745,7 +786,6 @@ router.get('/:slug/photo/:photoId',
|
|||||||
|
|
||||||
// Handle video streaming with range requests
|
// Handle video streaming with range requests
|
||||||
if (isVideo) {
|
if (isVideo) {
|
||||||
const fs = require('fs');
|
|
||||||
const stat = fs.statSync(filePath);
|
const stat = fs.statSync(filePath);
|
||||||
const fileSize = stat.size;
|
const fileSize = stat.size;
|
||||||
const range = req.headers.range;
|
const range = req.headers.range;
|
||||||
@@ -789,7 +829,6 @@ router.get('/:slug/photo/:photoId',
|
|||||||
|
|
||||||
// Generate ETag based on photo id, modification time, and watermark settings
|
// Generate ETag based on photo id, modification time, and watermark settings
|
||||||
// This ensures cache invalidation when watermark settings change
|
// This ensures cache invalidation when watermark settings change
|
||||||
const fs = require('fs');
|
|
||||||
const stat = fs.statSync(filePath);
|
const stat = fs.statSync(filePath);
|
||||||
const watermarkHash = watermarkSettings?.enabled
|
const watermarkHash = watermarkSettings?.enabled
|
||||||
? `-wm${watermarkSettings.opacity}${watermarkSettings.position}${watermarkSettings.size}`
|
? `-wm${watermarkSettings.opacity}${watermarkSettings.position}${watermarkSettings.size}`
|
||||||
@@ -806,7 +845,6 @@ router.get('/:slug/photo/:photoId',
|
|||||||
if (photo.watermark_path) {
|
if (photo.watermark_path) {
|
||||||
const watermarkFilePath = path.join(getStoragePath(), photo.watermark_path);
|
const watermarkFilePath = path.join(getStoragePath(), photo.watermark_path);
|
||||||
try {
|
try {
|
||||||
const fs = require('fs');
|
|
||||||
// Check if pre-generated watermark file exists
|
// Check if pre-generated watermark file exists
|
||||||
if (fs.existsSync(watermarkFilePath)) {
|
if (fs.existsSync(watermarkFilePath)) {
|
||||||
res.set({
|
res.set({
|
||||||
|
|||||||
@@ -96,12 +96,30 @@ export const PhotoUpload: React.FC<PhotoUploadProps> = ({ eventId, onUploadCompl
|
|||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
|
||||||
// For large uploads, chunk the files to prevent memory issues
|
// For large uploads, chunk the files by both count AND size to prevent memory/network issues
|
||||||
const CHUNK_SIZE = Math.max(1, Math.min(50, maxFilesPerUpload)); // Upload up to 50 (or limit) files at a time
|
const MAX_FILES_PER_CHUNK = Math.max(1, Math.min(50, maxFilesPerUpload)); // Max 50 files per chunk
|
||||||
const chunks = [];
|
const MAX_BYTES_PER_CHUNK = 500 * 1024 * 1024; // Max 500MB per chunk (nginx limit is 1GB)
|
||||||
|
const chunks: File[][] = [];
|
||||||
|
|
||||||
for (let i = 0; i < selectedFiles.length; i += CHUNK_SIZE) {
|
let currentChunk: File[] = [];
|
||||||
chunks.push(selectedFiles.slice(i, i + CHUNK_SIZE));
|
let currentChunkSize = 0;
|
||||||
|
|
||||||
|
for (const file of selectedFiles) {
|
||||||
|
// Start a new chunk if adding this file would exceed limits
|
||||||
|
if (currentChunk.length >= MAX_FILES_PER_CHUNK ||
|
||||||
|
(currentChunkSize + file.size > MAX_BYTES_PER_CHUNK && currentChunk.length > 0)) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
|
currentChunk = [];
|
||||||
|
currentChunkSize = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChunk.push(file);
|
||||||
|
currentChunkSize += file.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't forget the last chunk
|
||||||
|
if (currentChunk.length > 0) {
|
||||||
|
chunks.push(currentChunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTotalChunks(chunks.length);
|
setTotalChunks(chunks.length);
|
||||||
|
|||||||
Reference in New Issue
Block a user