const express = require('express'); const router = express.Router(); const path = require('path'); const fsp = require('fs/promises'); const db = require('../db/knex'); const wss = require('../ws-server'); // ----------------------------------------------------- // Normalizzazione record DB โ†’ formato identico al vecchio index.json // ----------------------------------------------------- function safeJsonParse(value, fallback) { try { return value ? JSON.parse(value) : fallback; } catch { return fallback; } } function normalizePhoto(p) { return { ...p, gps: (p.lat != null && p.lon != null) ? { lat: p.lat, lng: p.lon, alt: p.alt ?? null } : null, location: safeJsonParse(p.location, null), _indexHash: p._indexHash || null, lat: undefined, lon: undefined, alt: undefined, location_json: undefined }; } // ----------------------------------------------------- // File helpers hard delete foto // ----------------------------------------------------- const PUBLIC_ROOT = path.resolve(__dirname, '..', 'public'); const PHOTOS_ROOT = path.join(PUBLIC_ROOT, 'photos'); const DEL_PHOTOS_ROOT = path.join(PUBLIC_ROOT, 'del_photos'); function safeJoin(base, ...parts) { const resolvedBase = path.resolve(base); const resolved = path.resolve(base, ...parts); if (!resolved.startsWith(resolvedBase)) { throw new Error(`Path non sicuro: ${resolved}`); } return resolved; } function getOriginalPhotoPath(row) { return safeJoin( PHOTOS_ROOT, row.user, 'original', row.cartella || '', row.name ); } function getDeletedPhotoPath(row) { return safeJoin( DEL_PHOTOS_ROOT, row.user, 'original', row.cartella || '', row.name ); } function getThumbPath(row, thumbValue) { if (!thumbValue) return null; const value = String(thumbValue); // Caso 1: /photos/Fabio/thumbs/prime/thumb.jpg if (value.startsWith('/photos/')) { const relative = value.replace(/^\/+/, ''); return safeJoin(PUBLIC_ROOT, relative); } // Caso 2: photos/Fabio/thumbs/prime/thumb.jpg if (value.startsWith('photos/')) { return safeJoin(PUBLIC_ROOT, value); } // Caso 3: solo nome file thumb.jpg return safeJoin( PHOTOS_ROOT, row.user, 'thumbs', row.cartella || '', value ); } async function pathExists(filePath) { try { await fsp.access(filePath); return true; } catch { return false; } } async function moveFileSafe(src, dst) { if (!src || !dst) return false; const exists = await pathExists(src); if (!exists) { console.warn(`โš ๏ธ File originale non trovato, skip move: ${src}`); return false; } // Crea automaticamente public/del_photos//original// await fsp.mkdir(path.dirname(dst), { recursive: true }); try { await fsp.rename(src, dst); return true; } catch (err) { // Fallback se source/destination sono su filesystem diversi if (err.code === 'EXDEV') { await fsp.copyFile(src, dst); await fsp.unlink(src); return true; } throw err; } } async function deleteFileIfExists(filePath) { if (!filePath) return false; try { await fsp.unlink(filePath); return true; } catch (err) { if (err.code === 'ENOENT') { return false; } throw err; } } // ----------------------------------------------------- // POST /photos // Salva una foto inviata dallo scanner locale // ----------------------------------------------------- router.post('/', async (req, res) => { const start = Date.now(); console.log(`๐Ÿ“ฅ [POST /photos] Richiesta at=${new Date().toISOString()}`); console.log('๐Ÿ“ฅ Body:', req.body); try { const p = req.body; console.log(`๐Ÿ“ธ [DB INSERT] Salvo foto id=${p.id} user=${p.user}`); await db('photos') .insert({ id: p.id, user: p.user, cartella: p.cartella, name: p.name, path: p.path, thub1: p.thub1, thub2: p.thub2, mime_type: p.mime_type, width: p.width, height: p.height, rotation: p.rotation, size_bytes: p.size_bytes, mtimeMs: p.mtimeMs, duration_ms: p.duration_ms, taken_at: p.taken_at, data: p.data, lat: p.gps?.lat ?? null, lon: p.gps?.lng ?? null, alt: p.gps?.alt ?? null, location: p.location ? JSON.stringify(p.location) : null, _indexHash: p._indexHash, fast_hash: p.fast_hash, deleted_at: p.deleted_at || null, created_at: new Date().toISOString(), updated_at: new Date().toISOString() }) .onConflict('id') .merge(); wss.broadcastToUser(p.user, { type: 'added', id: p.id, entity: 'photo' }); console.log(`โœ… [POST /photos] Foto id=${p.id} salvata durata=${Date.now() - start}ms`); res.json({ ok: true }); } catch (err) { console.error('โŒ Errore POST /photos:', err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // GET /photos/changes // Progressive sync ADD + UPDATE // ----------------------------------------------------- router.get('/changes', async (req, res) => { const user = req.user.name; const since = req.query.since; const start = Date.now(); console.log( `๐Ÿ”„ [SYNC SINCE] GET /photos/changes user=${user} since=${since} at=${new Date().toISOString()}` ); if (!since) { console.log('โš ๏ธ [SYNC SINCE] Richiesta senza ?since'); return res.status(400).json({ error: 'Missing ?since=timestamp' }); } try { let rows; if (user === 'Admin') { rows = await db('photos') .where(builder => { builder .where('created_at', '>', since) .orWhere('updated_at', '>', since); }) .orderBy('updated_at', 'desc'); } else { rows = await db('photos') .whereIn('user', [user, 'Common']) .andWhere(builder => { builder .where('created_at', '>', since) .orWhere('updated_at', '>', since); }) .orderBy('updated_at', 'desc'); } console.log( `๐Ÿ”„ [SYNC SINCE] Risposta: ${rows.length} cambiamenti durata=${Date.now() - start}ms` ); res.json(rows.map(normalizePhoto)); } catch (err) { console.error('โŒ Errore GET /photos/changes:', err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // GET /photos/byIds // ----------------------------------------------------- router.get('/byIds', async (req, res) => { const ids = req.query.id; const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user]; const start = Date.now(); console.log(`๐Ÿ“ค [GET /photos/byIds] at=${new Date().toISOString()}`); console.log('๐Ÿ“ค ids:', ids); console.log('๐Ÿ“ค users:', users); if (!ids) return res.json([]); const list = Array.isArray(ids) ? ids : [ids]; try { let rows; if (users.includes('Admin')) { rows = await db('photos').whereIn('id', list); } else { rows = await db('photos') .whereIn('id', list) .whereIn('user', users); } console.log( `๐Ÿ“ค [GET /photos/byIds] trovate ${rows.length} foto durata=${Date.now() - start}ms` ); res.json(rows.map(normalizePhoto)); } catch (err) { console.error('โŒ Errore /photos/byIds:', err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // GET /photos // Full sync // ----------------------------------------------------- router.get('/', async (req, res) => { const loggedUser = req.user?.name; const start = Date.now(); console.log( `๐Ÿ“ค [FULL SYNC] GET /photos da=${loggedUser} at=${new Date().toISOString()}` ); try { let rows; if (loggedUser === 'Admin') { rows = await db('photos') .select('*') .orderBy('mtimeMs', 'desc'); } else { rows = await db('photos') .whereIn('user', [loggedUser, 'Common']) .orderBy('mtimeMs', 'desc'); } console.log( `๐Ÿ“ค [FULL SYNC] Risposta: ${rows.length} foto durata=${Date.now() - start}ms` ); res.json(rows.map(normalizePhoto)); } catch (err) { console.error('โŒ Errore GET /photos:', err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // POST /photos/initDB // Reset DB foto // - Admin: cancella tutte le foto // - Utente normale: cancella solo le proprie // ----------------------------------------------------- router.post('/initDB', async (req, res) => { try { const loggedUser = req.user?.name; if (!loggedUser) { return res.status(401).json({ ok: false, error: 'Unauthorized' }); } console.log(`๐Ÿงน [POST /photos/initDB] richiesto da=${loggedUser}`); if (loggedUser === 'Admin') { console.log('๐Ÿงน Admin โ†’ cancello tutte le foto'); await db('photos').del(); await db('deleted_hard').del(); return res.json({ ok: true, scope: 'all', user: loggedUser, message: 'DB foto svuotato completamente' }); } console.log(`๐Ÿงน User=${loggedUser} โ†’ cancello solo le sue foto`); await db('photos') .where({ user: loggedUser }) .del(); await db('deleted_hard') .where({ user: loggedUser }) .del(); res.json({ ok: true, scope: 'user', user: loggedUser, message: `DB foto svuotato per user=${loggedUser}` }); } catch (err) { console.error('โŒ Errore POST /photos/initDB:', err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // POST /photos/initDBuser // Reset DB foto per utente specifico // Solo Admin // ----------------------------------------------------- router.post('/initDBuser', async (req, res) => { try { const loggedUser = req.user?.name; if (loggedUser !== 'Admin') { return res.status(403).json({ ok: false, error: 'Forbidden', loggedUser }); } const targetUser = req.body?.user || req.query?.user || req.body?.name || req.query?.name; if (!targetUser) { return res.status(400).json({ ok: false, error: 'Missing user' }); } console.log( `๐Ÿงน [POST /photos/initDBuser] Admin=${loggedUser} target=${targetUser}` ); await db('photos') .where({ user: targetUser }) .del(); await db('deleted_hard') .where({ user: targetUser }) .del(); res.json({ ok: true, user: targetUser, message: `DB foto svuotato per user=${targetUser}` }); } catch (err) { console.error('โŒ Errore POST /photos/initDBuser:', err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // GET /photos/deleted_hard // Deve stare PRIMA di /:id // ----------------------------------------------------- router.get('/deleted_hard', async (req, res) => { const since = req.query.since; const user = req.user?.name; try { let rows; if (user === 'Admin') { rows = await db('deleted_hard') .where('deleted_at', '>', since); } else { rows = await db('deleted_hard') .where('deleted_at', '>', since) .where({ user }); } res.json({ deleted: rows }); } catch (err) { console.error('โŒ Errore GET /photos/deleted_hard:', err); res.status(500).json({ error: err.message }); } }); // ----------------------------------------------------- // POST /photos/:id/toggle-soft // Toggle soft delete foto per ID // - Se deleted_at รจ null โ†’ soft delete // - Se deleted_at รจ valorizzato โ†’ restore // ----------------------------------------------------- router.post('/:id/toggle-soft', async (req, res) => { const id = req.params.id; const loggedUser = req.user?.name; const start = Date.now(); try { if (!loggedUser) { return res.status(401).json({ ok: false, error: 'Unauthorized' }); } console.log(`๐Ÿ” [TOGGLE SOFT /photos/${id}] richiesto da=${loggedUser}`); let row; if (loggedUser === 'Admin') { row = await db('photos') .where({ id }) .first(); } else { row = await db('photos') .where({ id, user: loggedUser }) .first(); } if (!row) { return res.status(404).json({ ok: false, error: 'Photo not found or not allowed', id }); } const now = new Date().toISOString(); const nextDeletedAt = row.deleted_at ? null : now; await db('photos') .where({ id }) .update({ deleted_at: nextDeletedAt, updated_at: now }); try { wss.broadcastToUser(row.user, { type: 'updated', id, entity: 'photo', deleted_at: nextDeletedAt }); } catch (e) { console.warn('โš ๏ธ WS broadcast photo toggle soft fallito:', e.message); } console.log( `โœ… [TOGGLE SOFT /photos/${id}] user=${row.user} soft_deleted=${!!nextDeletedAt} durata=${Date.now() - start}ms` ); res.json({ ok: true, id, user: row.user, deleted_at: nextDeletedAt, soft_deleted: !!nextDeletedAt, restored: !nextDeletedAt, duration_ms: Date.now() - start }); } catch (err) { console.error('โŒ Errore POST /photos/:id/toggle-soft:', err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // DELETE /photos/:id // HARD DELETE foto per ID // - Sposta originale in public/del_photos//original// // - Elimina thub1 e thub2 // - Rimuove record da photos // - Registra ID in deleted_hard // ----------------------------------------------------- router.delete('/:id', async (req, res) => { const id = req.params.id; const loggedUser = req.user?.name; const start = Date.now(); try { if (!loggedUser) { return res.status(401).json({ ok: false, error: 'Unauthorized' }); } console.log(`๐Ÿ—‘๏ธ [HARD DELETE /photos/${id}] richiesto da=${loggedUser}`); let row; if (loggedUser === 'Admin') { row = await db('photos') .where({ id }) .first(); } else { row = await db('photos') .where({ id, user: loggedUser }) .first(); } if (!row) { return res.status(404).json({ ok: false, error: 'Photo not found or not allowed', id }); } const deletedAt = new Date().toISOString(); // 1. Sposta originale const originalSrc = getOriginalPhotoPath(row); const originalDst = getDeletedPhotoPath(row); const originalMoved = await moveFileSafe(originalSrc, originalDst); // 2. Elimina thumbs const thumb1Path = getThumbPath(row, row.thub1); const thumb2Path = getThumbPath(row, row.thub2); const thumb1Deleted = await deleteFileIfExists(thumb1Path); const thumb2Deleted = await deleteFileIfExists(thumb2Path); // 3. Elimina record da photos await db('photos') .where({ id }) .del(); // 4. Registra hard delete await db('deleted_hard') .insert({ id, user: row.user, deleted_at: deletedAt }) .onConflict('id') .merge(); // 5. WS realtime try { wss.broadcastToUser(row.user, { type: 'removed', id, entity: 'photo' }); } catch (e) { console.warn('โš ๏ธ WS broadcast photo hard delete fallito:', e.message); } console.log( `โœ… [HARD DELETE /photos/${id}] user=${row.user} durata=${Date.now() - start}ms` ); res.json({ ok: true, id, user: row.user, deleted_at: deletedAt, hard_deleted: true, files: { original_src: originalSrc, original_dst: originalDst, original_moved: originalMoved, thumb1: thumb1Path, thumb1_deleted: thumb1Deleted, thumb2: thumb2Path, thumb2_deleted: thumb2Deleted }, duration_ms: Date.now() - start }); } catch (err) { console.error('โŒ Errore HARD DELETE /photos/:id:', err); res.status(500).json({ ok: false, error: err.message }); } }); // ----------------------------------------------------- // GET /photos/:id // Deve stare DOPO changes, byIds, initDB, deleted_hard, toggle-soft e delete // ----------------------------------------------------- router.get('/:id', async (req, res) => { const id = req.params.id; const loggedUser = req.user?.name; const start = Date.now(); console.log( `๐Ÿ“ค [GET /photos/${id}] Richiesta da=${loggedUser} at=${new Date().toISOString()}` ); try { let rows; if (loggedUser === 'Admin') { rows = await db('photos').where({ id }); } else { rows = await db('photos') .where({ id }) .whereIn('user', [loggedUser, 'Common']); } console.log( `๐Ÿ“ค [GET /photos/${id}] Risposta: ${rows.length} foto durata=${Date.now() - start}ms` ); if (!rows.length) return res.json([]); res.json(rows.map(normalizePhoto)); } catch (err) { console.error('โŒ Errore GET /photos/:id:', err); res.status(500).json({ error: err.message }); } }); module.exports = router;