Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 59651b8c24 | |||
| 7ccd48297f | |||
| d05ff6380e | |||
| 605f773a7e | |||
| c844f634c8 | |||
| 1d94398e2d | |||
| a2551dc0ad | |||
| 32821934e6 |
@@ -11,8 +11,7 @@
|
|||||||
|
|
||||||
**PicPeak** is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Designed specifically for photographers and event organizers, PicPeak makes it simple to share beautiful, time-limited photo galleries with clients while maintaining full control over your data and branding.
|
**PicPeak** is a powerful, self-hosted open-source alternative to commercial photo-sharing platforms like PicDrop.com and Scrapbook.de. Designed specifically for photographers and event organizers, PicPeak makes it simple to share beautiful, time-limited photo galleries with clients while maintaining full control over your data and branding.
|
||||||
|
|
||||||

|

|
||||||
> 📸 *Gallery preview will be updated soon with latest interface*
|
|
||||||
|
|
||||||
## 🌟 Why Choose PicPeak?
|
## 🌟 Why Choose PicPeak?
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.35",
|
"version": "1.0.39",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.35",
|
"version": "1.0.39",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-backend",
|
"name": "picpeak-backend",
|
||||||
"version": "1.0.35",
|
"version": "1.0.39",
|
||||||
"description": "Backend for PicPeak event photo sharing platform",
|
"description": "Backend for PicPeak event photo sharing platform",
|
||||||
"main": "server.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Storage Management Scripts
|
||||||
|
|
||||||
|
## check-storage.js
|
||||||
|
|
||||||
|
Checks the storage directory structure and verifies that photo files exist.
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
```bash
|
||||||
|
# Check overall storage structure
|
||||||
|
node scripts/check-storage.js
|
||||||
|
|
||||||
|
# Check specific event
|
||||||
|
node scripts/check-storage.js wedding-test-gallery-2025-07-14-1
|
||||||
|
```
|
||||||
|
|
||||||
|
### What it checks:
|
||||||
|
- Storage directory structure and permissions
|
||||||
|
- Event directories
|
||||||
|
- Photo files existence
|
||||||
|
- Thumbnail files existence
|
||||||
|
- Database vs filesystem consistency
|
||||||
|
|
||||||
|
## regenerate-thumbnails.js
|
||||||
|
|
||||||
|
Regenerates missing thumbnails for photos in the database.
|
||||||
|
|
||||||
|
### Usage:
|
||||||
|
```bash
|
||||||
|
# Regenerate all missing thumbnails
|
||||||
|
node scripts/regenerate-thumbnails.js
|
||||||
|
|
||||||
|
# Regenerate thumbnails for specific event (by ID)
|
||||||
|
node scripts/regenerate-thumbnails.js 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### What it does:
|
||||||
|
- Scans photos in database
|
||||||
|
- Checks if thumbnails exist
|
||||||
|
- Generates missing thumbnails using Sharp
|
||||||
|
- Updates database with thumbnail paths
|
||||||
|
- Reports success/error statistics
|
||||||
|
|
||||||
|
### Prerequisites:
|
||||||
|
- Node.js environment
|
||||||
|
- Database access
|
||||||
|
- Write permissions to storage directory
|
||||||
|
- Sharp library installed
|
||||||
|
|
||||||
|
## Production Usage
|
||||||
|
|
||||||
|
On your production server:
|
||||||
|
|
||||||
|
1. First, check the storage structure:
|
||||||
|
```bash
|
||||||
|
cd /path/to/picpeak/backend
|
||||||
|
NODE_ENV=production node scripts/check-storage.js wedding-test-gallery-2025-07-14-1
|
||||||
|
```
|
||||||
|
|
||||||
|
2. If thumbnails are missing, regenerate them:
|
||||||
|
```bash
|
||||||
|
NODE_ENV=production node scripts/regenerate-thumbnails.js 2
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
Executable
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to check storage directory structure and verify files
|
||||||
|
* Usage: node scripts/check-storage.js [eventSlug]
|
||||||
|
*/
|
||||||
|
|
||||||
|
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');
|
||||||
|
|
||||||
|
async function checkDirectory(dirPath, description) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
const stats = await fs.stat(dirPath);
|
||||||
|
const files = await fs.readdir(dirPath);
|
||||||
|
console.log(`✓ ${description}: ${dirPath}`);
|
||||||
|
console.log(` - Files/Folders: ${files.length}`);
|
||||||
|
console.log(` - Permissions: ${(stats.mode & parseInt('777', 8)).toString(8)}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`✗ ${description}: ${dirPath} - ${error.message}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkStorageStructure(eventSlug = null) {
|
||||||
|
console.log('Checking storage structure...');
|
||||||
|
console.log(`Storage base path: ${STORAGE_PATH}\n`);
|
||||||
|
|
||||||
|
// Check main directories
|
||||||
|
await checkDirectory(STORAGE_PATH, 'Storage root');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'events'), 'Events directory');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'events/active'), 'Active events');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'events/archived'), 'Archived events');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'thumbnails'), 'Thumbnails');
|
||||||
|
await checkDirectory(path.join(STORAGE_PATH, 'uploads'), 'Uploads');
|
||||||
|
|
||||||
|
console.log('\n---\n');
|
||||||
|
|
||||||
|
// If event slug provided, check specific event
|
||||||
|
if (eventSlug) {
|
||||||
|
console.log(`Checking specific event: ${eventSlug}`);
|
||||||
|
|
||||||
|
const event = await db('events').where('slug', eventSlug).first();
|
||||||
|
if (!event) {
|
||||||
|
console.log(`✗ Event not found in database: ${eventSlug}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Event found in database:`);
|
||||||
|
console.log(` - ID: ${event.id}`);
|
||||||
|
console.log(` - Name: ${event.event_name}`);
|
||||||
|
console.log(` - Active: ${event.is_active}`);
|
||||||
|
console.log(` - Archived: ${event.is_archived}`);
|
||||||
|
|
||||||
|
// Check event directory
|
||||||
|
const eventDir = path.join(STORAGE_PATH, 'events/active', eventSlug);
|
||||||
|
const eventExists = await checkDirectory(eventDir, 'Event directory');
|
||||||
|
|
||||||
|
if (eventExists) {
|
||||||
|
const files = await fs.readdir(eventDir);
|
||||||
|
console.log(` - Photo files: ${files.filter(f => /\.(jpg|jpeg|png|gif)$/i.test(f)).length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check photos in database
|
||||||
|
const photos = await db('photos').where('event_id', event.id).select('id', 'filename', 'path', 'thumbnail_path');
|
||||||
|
console.log(`\nDatabase photos: ${photos.length}`);
|
||||||
|
|
||||||
|
// Check if photo files exist
|
||||||
|
let existingPhotos = 0;
|
||||||
|
let missingPhotos = 0;
|
||||||
|
let existingThumbnails = 0;
|
||||||
|
let missingThumbnails = 0;
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
const photoPath = path.join(STORAGE_PATH, 'events/active', photo.path);
|
||||||
|
try {
|
||||||
|
await fs.access(photoPath);
|
||||||
|
existingPhotos++;
|
||||||
|
} catch {
|
||||||
|
missingPhotos++;
|
||||||
|
console.log(` ✗ Missing photo: ${photo.path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (photo.thumbnail_path) {
|
||||||
|
const thumbPath = path.join(STORAGE_PATH, photo.thumbnail_path.replace(/^\//, ''));
|
||||||
|
try {
|
||||||
|
await fs.access(thumbPath);
|
||||||
|
existingThumbnails++;
|
||||||
|
} catch {
|
||||||
|
missingThumbnails++;
|
||||||
|
console.log(` ✗ Missing thumbnail: ${photo.thumbnail_path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\nFile check summary:`);
|
||||||
|
console.log(` - Photos: ${existingPhotos} exist, ${missingPhotos} missing`);
|
||||||
|
console.log(` - Thumbnails: ${existingThumbnails} exist, ${missingThumbnails} missing`);
|
||||||
|
} else {
|
||||||
|
// List all event directories
|
||||||
|
try {
|
||||||
|
const activeDir = path.join(STORAGE_PATH, 'events/active');
|
||||||
|
const eventDirs = await fs.readdir(activeDir);
|
||||||
|
console.log(`Active event directories: ${eventDirs.length}`);
|
||||||
|
for (const dir of eventDirs.slice(0, 10)) {
|
||||||
|
console.log(` - ${dir}`);
|
||||||
|
}
|
||||||
|
if (eventDirs.length > 10) {
|
||||||
|
console.log(` ... and ${eventDirs.length - 10} more`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Could not list event directories:', error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const eventSlug = process.argv[2] || null;
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
checkStorageStructure(eventSlug).then(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
console.log('\nStorage check complete');
|
||||||
|
}).catch(async error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
await db.destroy();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Executable
+107
@@ -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);
|
||||||
|
});
|
||||||
Executable
+132
@@ -0,0 +1,132 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to diagnose thumbnail serving issues
|
||||||
|
* Usage: node scripts/diagnose-thumbnails.js <eventId>
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 <eventId>');
|
||||||
|
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);
|
||||||
|
});
|
||||||
Executable
+141
@@ -0,0 +1,141 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to regenerate missing thumbnails for photos in the database
|
||||||
|
* Usage: node scripts/regenerate-thumbnails.js [eventId]
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
const sharp = require('sharp');
|
||||||
|
const { db } = require('../src/database/db');
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const THUMBNAIL_SIZE = 300;
|
||||||
|
const STORAGE_PATH = process.env.STORAGE_PATH || path.join(__dirname, '../../storage');
|
||||||
|
const THUMBNAILS_DIR = path.join(STORAGE_PATH, 'thumbnails');
|
||||||
|
|
||||||
|
async function ensureDirectoryExists(dirPath) {
|
||||||
|
try {
|
||||||
|
await fs.access(dirPath);
|
||||||
|
} catch {
|
||||||
|
await fs.mkdir(dirPath, { recursive: true });
|
||||||
|
console.log(`Created directory: ${dirPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateThumbnail(photoPath, thumbnailPath) {
|
||||||
|
try {
|
||||||
|
await sharp(photoPath)
|
||||||
|
.resize(THUMBNAIL_SIZE, THUMBNAIL_SIZE, {
|
||||||
|
fit: 'cover',
|
||||||
|
position: 'center'
|
||||||
|
})
|
||||||
|
.jpeg({ quality: 80 })
|
||||||
|
.toFile(thumbnailPath);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to generate thumbnail for ${photoPath}:`, error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function regenerateThumbnails(eventId = null) {
|
||||||
|
try {
|
||||||
|
console.log('Starting thumbnail regeneration...');
|
||||||
|
console.log(`Storage path: ${STORAGE_PATH}`);
|
||||||
|
console.log(`Thumbnails directory: ${THUMBNAILS_DIR}`);
|
||||||
|
|
||||||
|
// Ensure thumbnails directory exists
|
||||||
|
await ensureDirectoryExists(THUMBNAILS_DIR);
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
let query = db('photos')
|
||||||
|
.join('events', 'photos.event_id', 'events.id')
|
||||||
|
.select(
|
||||||
|
'photos.id',
|
||||||
|
'photos.filename',
|
||||||
|
'photos.path',
|
||||||
|
'photos.thumbnail_path',
|
||||||
|
'events.slug as event_slug'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventId) {
|
||||||
|
query = query.where('photos.event_id', eventId);
|
||||||
|
console.log(`Filtering for event ID: ${eventId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const photos = await query;
|
||||||
|
console.log(`Found ${photos.length} photos to process`);
|
||||||
|
|
||||||
|
let successCount = 0;
|
||||||
|
let skipCount = 0;
|
||||||
|
let errorCount = 0;
|
||||||
|
|
||||||
|
for (const photo of photos) {
|
||||||
|
const photoPath = path.join(STORAGE_PATH, 'events/active', photo.path);
|
||||||
|
const thumbnailFilename = `thumb_${photo.filename}`;
|
||||||
|
const thumbnailPath = path.join(THUMBNAILS_DIR, thumbnailFilename);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check if photo file exists
|
||||||
|
await fs.access(photoPath);
|
||||||
|
|
||||||
|
// Check if thumbnail already exists
|
||||||
|
try {
|
||||||
|
await fs.access(thumbnailPath);
|
||||||
|
console.log(`Thumbnail already exists for ${photo.filename}, skipping...`);
|
||||||
|
skipCount++;
|
||||||
|
continue;
|
||||||
|
} catch {
|
||||||
|
// Thumbnail doesn't exist, generate it
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generating thumbnail for ${photo.filename}...`);
|
||||||
|
const success = await generateThumbnail(photoPath, thumbnailPath);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Update database with thumbnail path
|
||||||
|
await db('photos')
|
||||||
|
.where('id', photo.id)
|
||||||
|
.update({
|
||||||
|
thumbnail_path: `thumbnails/${thumbnailFilename}`
|
||||||
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
|
console.log(`✓ Generated thumbnail for ${photo.filename}`);
|
||||||
|
} else {
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`✗ Photo file not found: ${photoPath}`);
|
||||||
|
errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('\nThumbnail regeneration complete!');
|
||||||
|
console.log(`- Successfully generated: ${successCount}`);
|
||||||
|
console.log(`- Skipped (already exist): ${skipCount}`);
|
||||||
|
console.log(`- Errors: ${errorCount}`);
|
||||||
|
console.log(`- Total processed: ${photos.length}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during thumbnail regeneration:', error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await db.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const eventId = process.argv[2] ? parseInt(process.argv[2]) : null;
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
regenerateThumbnails(eventId).then(() => {
|
||||||
|
console.log('Script completed successfully');
|
||||||
|
process.exit(0);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Script failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Executable
+86
@@ -0,0 +1,86 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to test photo authentication
|
||||||
|
* Usage: node scripts/test-photo-auth.js <jwt-token>
|
||||||
|
*/
|
||||||
|
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
async function testPhotoAuth(token) {
|
||||||
|
if (!token) {
|
||||||
|
console.error('Usage: node scripts/test-photo-auth.js <jwt-token>');
|
||||||
|
console.error('\nTo get a token, login to a gallery and check localStorage for gallery_token_<slug>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = process.env.API_URL || 'http://localhost:3001';
|
||||||
|
|
||||||
|
console.log(`Testing photo authentication with token: ${token.substring(0, 20)}...`);
|
||||||
|
console.log(`Base URL: ${baseUrl}\n`);
|
||||||
|
|
||||||
|
// Test URLs
|
||||||
|
const tests = [
|
||||||
|
{
|
||||||
|
name: 'Thumbnail via static route',
|
||||||
|
url: `${baseUrl}/thumbnails/thumb_Test_Gallery_uncategorized_5210.jpg`,
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Photo via static route',
|
||||||
|
url: `${baseUrl}/photos/wedding-test-gallery-2025-07-14-1/Test_Gallery_uncategorized_5210.jpg`,
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Gallery photos API',
|
||||||
|
url: `${baseUrl}/api/gallery/wedding-test-gallery-2025-07-14-1/photos`,
|
||||||
|
headers: { 'Authorization': `Bearer ${token}` }
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
console.log(`Testing: ${test.name}`);
|
||||||
|
console.log(`URL: ${test.url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(test.url, {
|
||||||
|
headers: test.headers,
|
||||||
|
validateStatus: () => true // Don't throw on any status
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Status: ${response.status}`);
|
||||||
|
console.log(`Headers:`, response.headers['content-type']);
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
if (test.name.includes('API')) {
|
||||||
|
console.log(`Photos count: ${response.data.photos?.length || 0}`);
|
||||||
|
} else {
|
||||||
|
console.log(`Content length: ${response.headers['content-length']} bytes`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`Error:`, response.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Network error:`, error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('---\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode token to show info
|
||||||
|
try {
|
||||||
|
const parts = token.split('.');
|
||||||
|
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
||||||
|
console.log('Token payload:', payload);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Failed to decode token');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token from command line
|
||||||
|
const token = process.argv[2];
|
||||||
|
|
||||||
|
testPhotoAuth(token).catch(error => {
|
||||||
|
console.error('Test failed:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -363,12 +363,29 @@ router.delete('/:id', adminAuth, async (req, res) => {
|
|||||||
// Delete archive file if exists
|
// Delete archive file if exists
|
||||||
if (archive.archive_path) {
|
if (archive.archive_path) {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
console.error('Failed to delete archive file:', 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)
|
// Delete from database (cascade will delete photos and logs)
|
||||||
await db('events').where('id', req.params.id).delete();
|
await db('events').where('id', req.params.id).delete();
|
||||||
|
|
||||||
|
|||||||
@@ -136,8 +136,8 @@ router.get('/:slug/photos', verifyGalleryAccess, async (req, res) => {
|
|||||||
photos: photos.map(photo => ({
|
photos: photos.map(photo => ({
|
||||||
id: photo.id,
|
id: photo.id,
|
||||||
filename: photo.filename,
|
filename: photo.filename,
|
||||||
url: `/photos/${photo.path}`,
|
url: `/api/gallery/${req.params.slug}/photo/${photo.id}`,
|
||||||
thumbnail_url: photo.thumbnail_path ? `/${photo.thumbnail_path}` : null,
|
thumbnail_url: photo.thumbnail_path ? `/api/gallery/${req.params.slug}/thumbnail/${photo.id}` : null,
|
||||||
type: photo.type,
|
type: photo.type,
|
||||||
category_id: photo.category_id,
|
category_id: photo.category_id,
|
||||||
category_name: photo.category_name,
|
category_name: photo.category_name,
|
||||||
@@ -320,6 +320,42 @@ router.get('/:slug/photo/:photoId', verifyGalleryAccess, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Serve thumbnail
|
||||||
|
router.get('/:slug/thumbnail/:photoId', verifyGalleryAccess, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { photoId } = req.params;
|
||||||
|
|
||||||
|
const photo = await db('photos')
|
||||||
|
.where({ id: photoId, event_id: req.event.id })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
if (!photo || !photo.thumbnail_path) {
|
||||||
|
return res.status(404).json({ error: 'Thumbnail not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const thumbPath = path.join(getStoragePath(), photo.thumbnail_path);
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
const fs = require('fs').promises;
|
||||||
|
try {
|
||||||
|
await fs.access(thumbPath);
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(404).json({ error: 'Thumbnail file not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate headers
|
||||||
|
res.setHeader('Content-Type', 'image/jpeg');
|
||||||
|
res.setHeader('Cache-Control', 'private, max-age=3600');
|
||||||
|
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
|
||||||
|
|
||||||
|
// Send file
|
||||||
|
res.sendFile(path.resolve(thumbPath));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error serving thumbnail:', error);
|
||||||
|
res.status(500).json({ error: 'Failed to serve thumbnail' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get photo stats
|
// Get photo stats
|
||||||
router.get('/:slug/stats', verifyGalleryAccess, async (req, res) => {
|
router.get('/:slug/stats', verifyGalleryAccess, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Nginx Configuration Fix for Photo Authentication
|
||||||
|
|
||||||
|
If photos and thumbnails are not loading in gallery view but work in admin, it's likely that the Authorization header is being stripped by nginx or another reverse proxy.
|
||||||
|
|
||||||
|
## Common Issue
|
||||||
|
|
||||||
|
The `Authorization` header is often not passed through by default in nginx proxy configurations.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
Add these lines to your nginx configuration for the PicPeak location block:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
proxy_pass http://localhost:3001;
|
||||||
|
|
||||||
|
# Important: Pass the Authorization header
|
||||||
|
proxy_pass_header Authorization;
|
||||||
|
proxy_set_header Authorization $http_authorization;
|
||||||
|
|
||||||
|
# Other standard proxy headers
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative Fix Using Traefik
|
||||||
|
|
||||||
|
If using Traefik, ensure headers are passed:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
picpeak:
|
||||||
|
labels:
|
||||||
|
- "traefik.http.middlewares.picpeak-headers.headers.customrequestheaders.Authorization="
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
1. Check if Authorization header is reaching the backend:
|
||||||
|
```bash
|
||||||
|
curl -H "Authorization: Bearer YOUR_TOKEN" https://picpeak.yourdomain.com/thumbnails/test.jpg -v
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Check nginx logs to see if the header is present:
|
||||||
|
```bash
|
||||||
|
tail -f /var/log/nginx/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose Fix
|
||||||
|
|
||||||
|
If using docker-compose with nginx proxy, add:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
environment:
|
||||||
|
- NGINX_PROXY_PASS_HEADER=Authorization
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.35",
|
"version": "1.0.39",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"version": "1.0.35",
|
"version": "1.0.39",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.0.0",
|
"@tanstack/react-query": "^5.0.0",
|
||||||
"@tiptap/extension-link": "^2.25.0",
|
"@tiptap/extension-link": "^2.25.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "picpeak-frontend",
|
"name": "picpeak-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.0.35",
|
"version": "1.0.39",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -272,8 +272,8 @@ export const EventsListPage: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Events Table */}
|
{/* Events Table */}
|
||||||
<Card className="overflow-hidden">
|
<Card className="overflow-visible">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto overflow-y-visible">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-neutral-50 border-b border-neutral-200">
|
<thead className="bg-neutral-50 border-b border-neutral-200">
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user