// routes/autoScan.js const express = require('express'); const router = express.Router(); const path = require('path'); const fs = require('fs'); const fsp = require('fs/promises'); const db = require('../db/knex'); const { WEB_ROOT } = require('../api_v1/config'); const scanFile = require('../api_v1/scanner/scanFileEntry'); const scanPhotoSingle = require('../api_v1/scanner/scanPhotoSingle'); const scanCartella = require('../api_v1/scanner/scanCartella'); const scanActivitySingle = require('../api_v1/scanner/scanActivitySingle'); const scanActivityCartella = require('../api_v1/scanner/scanActivityCartella'); const createCleanupFunctions = require('../api_v1/scanner/orphanCleanup'); const { deleteThumbsById } = createCleanupFunctions(db, parseInt(process.env.PHOTO_RETENTION_DAYS || "30", 10)); const createDeleteFolderFunctions = require('../api_v1/scanner/deleteFolder'); const { deleteFolderForUser } = createDeleteFolderFunctions(db, parseInt(process.env.PHOTO_RETENTION_DAYS || "30", 10)); const wss = require('../ws-server'); // =============================== // Helpers // =============================== function relPath2Cartella(p) { const parts = p.split("/").filter(Boolean); return parts.slice(3).join("/"); } function getArea(relPath) { const parts = relPath.split('/').filter(Boolean); return parts[0]; } const { sha256 } = require('../api_v1/scanner/utils'); function activityId(user, cartella, file) { return sha256(`${user}/${cartella}/${file}`); } // =============================== // AUTO SCAN ROUTER // =============================== router.post('/', async (req, res) => { const { type, file, path: relPath, user } = req.body; const area = getArea(relPath); const cart = relPath2Cartella(relPath); const absFile = path.join( __dirname, '..', WEB_ROOT, relPath, file ); try { switch (type) { // =============================== // ADD_DIR — aggiunta cartella (bulk) // =============================== case 'ADD_DIR': { // =================================== // ACTIVITIES // =================================== if (area === 'activities') { const absCartella = path.join( __dirname, '..', WEB_ROOT, 'activities', user, 'original', file ); const newActivities = []; const CURRENT = { value: 0 }; const TOTAL_FILES = 0; const start = Date.now(); const since = new Date().toISOString(); await scanActivityCartella( db, user, file, absCartella, newActivities, async () => {}, CURRENT, TOTAL_FILES, start, deleteThumbsById, async () => {}, null ); wss.broadcastToUser(user, { type: "add_dir", mode: "activities", bulk: true, count: newActivities.length, since }); wss.broadcastToUser(user, { type: "add_dir_done", mode: "activities", folder: file, count: newActivities.length }); return res.json({ status: 'OK', action: 'ADD_DIR', entity: 'activities', count: newActivities.length }); } // =================================== // PHOTOS // =================================== const absCartella = path.join( __dirname, '..', WEB_ROOT, "photos", user, "original", file ); const BULK_THRESHOLD = parseInt(process.env.WS_BULK_THRESHOLD || "500", 10); let count = 0; let bulkMode = false; const since = new Date().toISOString(); for await (const f of scanCartella(user, file, absCartella, db)) { count++; await db('deleted_hard') .where({ id: f.id, user }) .del(); await db('photos') .where({ id: f.id, user }) .del(); const arr = []; await scanPhotoSingle( db, user, file, f, arr ); if (!bulkMode && count > BULK_THRESHOLD) { bulkMode = true; wss.broadcastToUser(user, { type: "add_dir", folder: file, mode: "bulk", count, since }); continue; } if (!bulkMode) { wss.broadcastToUser(user, { type: "added", id: f.id }); } } if (bulkMode) { wss.broadcastToUser(user, { type: "add_dir_done", folder: file, mode: "bulk", count, since }); } return res.json({ status: 'OK', action: 'ADD_DIR', count, bulkMode }); } // =============================== // DEL — singolo file // =============================== case 'DEL': { // =================================== // ACTIVITIES // =================================== /*if (area === 'activities') { const id = activityId( user, cart, file ); await db('activities') .where({ id, user }) .del(); await db('deleted_hard_activities') .insert({ id, user, deleted_at: new Date().toISOString() }) .onConflict('id') .merge(); wss.broadcastToUser(user, { type: "removed", mode: "activities", id }); return res.json({ status: 'OK', action: 'DEL', entity: 'activities', id }); }*/ if (area === 'activities') { // 1️⃣ Trovo l'attività in base a cartella + file_name const activity = await db('activities') .where({ user, cartella: cart, file_name: file }) .first(); if (!activity) { // Nessuna attività trovata: non c'è nulla da cancellare return res.json({ status: 'OK', action: 'DEL', entity: 'activities', skipped: true }); } const id = activity.id; // 2️⃣ Cancello dal DB principale await db('activities') .where({ id, user }) .del(); // 3️⃣ Segno la cancellazione hard await db('deleted_hard_activities') .insert({ id, user, deleted_at: new Date().toISOString() }) .onConflict('id') .merge(); // 4️⃣ Notifica WS wss.broadcastToUser(user, { type: "removed", mode: "activities", id }); return res.json({ status: 'OK', action: 'DEL', entity: 'activities', id }); } // =================================== // PHOTOS // =================================== const e = await scanFile(user, cart, absFile); await deleteThumbsById(e.id); await db('photos') .where({ id: e.id, user }) .del(); try { await db('deleted_hard') .insert({ id: e.id, user, deleted_at: new Date().toISOString() }) .onConflict('id') .merge(); } catch (err) { console.warn( "⚠️ deleted_hard già presente o errore non bloccante:", err.message ); } wss.broadcastToUser(user, { type: "del", id: e.id }); return res.json({ status: 'OK', action: 'DEL', id: e.id }); } // =============================== // ADD — singolo file // =============================== case 'ADD': { // =================================== // ACTIVITIES // =================================== if (area === 'activities') { const ext = path.extname(file).toLowerCase(); if (!['.fit', '.gpx', '.tcx'].includes(ext)) { return res.status(400).json({ error: `Estensione attività non supportata: ${ext}` }); } let st; try { st = await fsp.stat(absFile); } catch { return res.status(404).json({ error: 'File attività non trovato' }); } const f = { id: `${cart}/${file}`, name: file, file_name: file, relPath: `${cart}/${file}`, // ✔ percorso corretto absPath: absFile, ext, stat: st, path: `/activities/${user}/original/${cart}/${file}` }; const newActivities = []; const activity = await scanActivitySingle( db, user, cart, f, newActivities ); if (!activity) { return res.json({ status: 'OK', action: 'ADD', skipped: true }); } wss.broadcastToUser(user, { type: 'added', mode: 'activities', id: activity.id }); return res.json({ status: 'OK', action: 'ADD', entity: 'activities', id: activity.id }); } // =================================== // PHOTOS // =================================== const f = await scanFile( user, cart, absFile ); await db('deleted_hard') .where({ id: f.id, user }) .del(); await db('photos') .where({ id: f.id, user }) .del(); const newFiles = []; await scanPhotoSingle( db, user, cart, f, newFiles ); wss.broadcastToUser(user, { type: "added", id: f.id }); return res.json({ status: 'OK', action: 'ADD', id: f.id }); } // =============================== // DEL_DIR — rimozione cartella (bulk) // =============================== case 'DEL_DIR': { // =================================== // ACTIVITIES // =================================== if (area === 'activities') { const activities = await db('activities') .where({ user, cartella: file }); for (const a of activities) { await db('activities') .where({ id: a.id, user }) .del(); await db('deleted_hard_activities') .insert({ id: a.id, user, deleted_at: new Date().toISOString() }) .onConflict('id') .merge(); wss.broadcastToUser(user, { type: "removed", mode: "activities", id: a.id }); } return res.json({ status: "OK", action: "DEL_DIR", entity: "activities", count: activities.length }); } // =================================== // PHOTOS // =================================== const photos = await db('photos') .where({ user, cartella: file }); const BULK_THRESHOLD = parseInt(process.env.WS_BULK_THRESHOLD || "500", 10); const since = new Date().toISOString(); const bulkMode = photos.length > BULK_THRESHOLD; if (bulkMode) { wss.broadcastToUser(user, { type: "del_dir", folder: file, mode: "bulk", count: photos.length, since }); } for (const p of photos) { await db('photos') .where({ id: p.id, user }) .del(); await deleteThumbsById(p.id); await db('deleted_hard') .insert({ id: p.id, user, deleted_at: new Date().toISOString() }); if (!bulkMode) { wss.broadcastToUser(user, { type: "del", id: p.id }); } } if (bulkMode) { wss.broadcastToUser(user, { type: "del_dir_done", folder: file, mode: "bulk", count: photos.length, since }); } return res.json({ status: 'OK', action: 'DEL_DIR', folder: file, count: photos.length, bulkMode }); } default: return res.status(400).json({ error: 'Tipo non valido' }); } } catch (err) { console.error('Errore auto_scan:', err); res.status(500).json({ error: 'Errore durante auto_scan', details: err.message }); } }); module.exports = router;