Fix storage path issues and React error #130

Backend fixes:
- Add STORAGE_PATH environment variable support
- Fix absolute path references in all backend services
- Update Docker configuration with correct storage path

Frontend fixes:
- Remove individual ErrorBoundary wrappers to fix React error #130
- Remove unused ErrorBoundary import
- Simplify route structure to prevent component mounting issues

This resolves:
- 500 errors when creating events due to storage permission issues
- React error #130 that occurred during event creation
- Consistent storage path handling across all services

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-07 13:20:40 +02:00
parent 2e10374e2c
commit 2e7cba9e8a
7 changed files with 31 additions and 21 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ router.post('/', adminAuth, [
expires_at.setDate(expires_at.getDate() + expiration_days); expires_at.setDate(expires_at.getDate() + expiration_days);
// Create folder structure // Create folder structure
const eventPath = path.join(__dirname, '../../../storage/events/active', slug); const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const eventPath = path.join(storagePath, 'events/active', slug);
await fs.mkdir(path.join(eventPath, 'collages'), { recursive: true }); await fs.mkdir(path.join(eventPath, 'collages'), { recursive: true });
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true }); await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
+2 -1
View File
@@ -58,7 +58,8 @@ router.post('/', adminAuth, [
expires_at.setDate(expires_at.getDate() + expiration_days); expires_at.setDate(expires_at.getDate() + expiration_days);
// Create folder structure // Create folder structure
const eventPath = path.join(__dirname, '../../../storage/events/active', slug); const storagePath = process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const eventPath = path.join(storagePath, 'events/active', slug);
await fs.mkdir(path.join(eventPath, 'collages'), { recursive: true }); await fs.mkdir(path.join(eventPath, 'collages'), { recursive: true });
await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true }); await fs.mkdir(path.join(eventPath, 'individual'), { recursive: true });
+5 -2
View File
@@ -5,6 +5,9 @@ const archiver = require('archiver');
const path = require('path'); const path = require('path');
const router = express.Router(); const router = express.Router();
// Get storage path from environment or default
const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
// Middleware to verify gallery access // Middleware to verify gallery access
async function verifyGalleryAccess(req, res, next) { async function verifyGalleryAccess(req, res, next) {
try { try {
@@ -115,7 +118,7 @@ router.get('/:slug/download/:photoId', verifyGalleryAccess, async (req, res) =>
photo_id: photoId photo_id: photoId
}); });
const filePath = path.join(__dirname, '../../../storage/events/active', req.event.slug, photo.path); const filePath = path.join(getStoragePath(), 'events/active', req.event.slug, photo.path);
res.download(filePath, photo.filename); res.download(filePath, photo.filename);
} catch (error) { } catch (error) {
res.status(500).json({ error: 'Failed to download photo' }); res.status(500).json({ error: 'Failed to download photo' });
@@ -143,7 +146,7 @@ router.get('/:slug/download-all', verifyGalleryAccess, async (req, res) => {
// Add photos to archive // Add photos to archive
for (const photo of photos) { for (const photo of photos) {
const filePath = path.join(__dirname, '../../../storage/events/active', req.event.slug, photo.path); const filePath = path.join(getStoragePath(), 'events/active', req.event.slug, photo.path);
archive.file(filePath, { name: photo.path }); archive.file(filePath, { name: photo.path });
} }
+8 -7
View File
@@ -4,17 +4,18 @@ const path = require('path');
const { db } = require('../database/db'); const { db } = require('../database/db');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const ACTIVE_PATH = path.join(__dirname, '../../../storage/events/active'); const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const ARCHIVE_PATH = path.join(__dirname, '../../../storage/events/archived'); const ACTIVE_PATH = () => path.join(getStoragePath(), 'events/active');
const ARCHIVE_PATH = () => path.join(getStoragePath(), 'events/archived');
async function archiveEvent(event) { async function archiveEvent(event) {
try { try {
const eventPath = path.join(ACTIVE_PATH, event.slug); const eventPath = path.join(ACTIVE_PATH(), event.slug);
const archiveName = `${event.slug}.zip`; const archiveName = `${event.slug}.zip`;
const archivePath = path.join(ARCHIVE_PATH, archiveName); const archivePath = path.join(ARCHIVE_PATH(), archiveName);
// Ensure archive directory exists // Ensure archive directory exists
await fs.mkdir(ARCHIVE_PATH, { recursive: true }); await fs.mkdir(ARCHIVE_PATH(), { recursive: true });
// Create archive // Create archive
const output = require('fs').createWriteStream(archivePath); const output = require('fs').createWriteStream(archivePath);
@@ -32,7 +33,7 @@ async function archiveEvent(event) {
// Update database // Update database
await db('events').where('id', event.id).update({ await db('events').where('id', event.id).update({
is_archived: true, is_archived: true,
archive_path: path.relative(path.join(__dirname, '../../../storage'), archivePath), archive_path: path.relative(getStoragePath(), archivePath),
archived_at: new Date() archived_at: new Date()
}); });
@@ -43,7 +44,7 @@ async function archiveEvent(event) {
const photos = await db('photos').where('event_id', event.id); const photos = await db('photos').where('event_id', event.id);
for (const photo of photos) { for (const photo of photos) {
if (photo.thumbnail_path) { if (photo.thumbnail_path) {
const thumbPath = path.join(__dirname, '../../../storage', photo.thumbnail_path); const thumbPath = path.join(getStoragePath(), photo.thumbnail_path);
await fs.unlink(thumbPath).catch(() => {}); // Ignore if already deleted await fs.unlink(thumbPath).catch(() => {}); // Ignore if already deleted
} }
} }
+5 -4
View File
@@ -5,10 +5,11 @@ const { db } = require('../database/db');
const { generateThumbnail } = require('./imageProcessor'); const { generateThumbnail } = require('./imageProcessor');
const logger = require('../utils/logger'); const logger = require('../utils/logger');
const WATCH_PATH = path.join(__dirname, '../../../storage/events/active'); const getStoragePath = () => process.env.STORAGE_PATH || path.join(__dirname, '../../../storage');
const WATCH_PATH = () => path.join(getStoragePath(), 'events/active');
function startFileWatcher() { function startFileWatcher() {
const watcher = chokidar.watch(WATCH_PATH, { const watcher = chokidar.watch(WATCH_PATH(), {
ignored: /(^|[\/\\])\../, // ignore dotfiles ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true, persistent: true,
awaitWriteFinish: { awaitWriteFinish: {
@@ -37,7 +38,7 @@ function startFileWatcher() {
} }
async function processNewPhoto(filePath) { async function processNewPhoto(filePath) {
const relativePath = path.relative(WATCH_PATH, filePath); const relativePath = path.relative(WATCH_PATH(), filePath);
const pathParts = relativePath.split(path.sep); const pathParts = relativePath.split(path.sep);
if (pathParts.length < 2) return; // Not in correct folder structure if (pathParts.length < 2) return; // Not in correct folder structure
@@ -73,7 +74,7 @@ async function processNewPhoto(filePath) {
} }
async function removePhoto(filePath) { async function removePhoto(filePath) {
const relativePath = path.relative(WATCH_PATH, filePath); const relativePath = path.relative(WATCH_PATH(), filePath);
// Remove from database // Remove from database
await db('photos').where({ path: relativePath }).delete(); await db('photos').where({ path: relativePath }).delete();
+3
View File
@@ -20,6 +20,8 @@ services:
- SMTP_USER= - SMTP_USER=
- SMTP_PASS= - SMTP_PASS=
- EMAIL_FROM=noreply@photo-sharing.local - EMAIL_FROM=noreply@photo-sharing.local
# Storage path
- STORAGE_PATH=/app/storage
# Umami Analytics (optional) # Umami Analytics (optional)
- UMAMI_URL= - UMAMI_URL=
- UMAMI_WEBSITE_ID= - UMAMI_WEBSITE_ID=
@@ -92,6 +94,7 @@ services:
dockerfile: Dockerfile.dev dockerfile: Dockerfile.dev
environment: environment:
- NODE_ENV=development - NODE_ENV=development
- STORAGE_PATH=/app/storage
volumes: volumes:
- ./backend:/app - ./backend:/app
- /app/node_modules - /app/node_modules
+6 -6
View File
@@ -21,7 +21,7 @@ import {
SettingsPage SettingsPage
} from './pages/admin'; } from './pages/admin';
import { AdminLayout } from './components/admin'; import { AdminLayout } from './components/admin';
import { PageErrorBoundary, ErrorBoundary, OfflineIndicator, SkipLink } from './components/common'; import { PageErrorBoundary, OfflineIndicator, SkipLink } from './components/common';
// Create a client // Create a client
const queryClient = new QueryClient({ const queryClient = new QueryClient({
@@ -69,16 +69,16 @@ function App() {
<Routes> <Routes>
<Route path="login" element={<AdminLoginPage />} /> <Route path="login" element={<AdminLoginPage />} />
<Route element={<AdminLayout />}> <Route element={<AdminLayout />}>
<Route path="dashboard" element={<ErrorBoundary><AdminDashboard /></ErrorBoundary>} /> <Route path="dashboard" element={<AdminDashboard />} />
<Route path="events" element={<ErrorBoundary><EventsListPage /></ErrorBoundary>} /> <Route path="events" element={<EventsListPage />} />
<Route path="events/new" element={<ErrorBoundary><CreateEventPage /></ErrorBoundary>} /> <Route path="events/new" element={<CreateEventPage />} />
<Route path="events/:id" element={<ErrorBoundary><EventDetailsPage /></ErrorBoundary>} /> <Route path="events/:id" element={<EventDetailsPage />} />
<Route path="archives" element={<ArchivesPage />} /> <Route path="archives" element={<ArchivesPage />} />
<Route path="email" element={<EmailConfigPage />} /> <Route path="email" element={<EmailConfigPage />} />
<Route path="analytics" element={<AnalyticsPage />} /> <Route path="analytics" element={<AnalyticsPage />} />
<Route path="branding" element={<BrandingPage />} /> <Route path="branding" element={<BrandingPage />} />
<Route path="settings" element={<SettingsPage />} /> <Route path="settings" element={<SettingsPage />} />
<Route path="/" element={<Navigate to="/admin/dashboard" replace />} /> <Route path="" element={<Navigate to="/admin/dashboard" replace />} />
</Route> </Route>
</Routes> </Routes>
</AdminAuthProvider> </AdminAuthProvider>