// scanner/scanPhoto.js const path = require('path'); const fsp = require('fs/promises'); const { log } = require('./logger'); const { loadPreviousIndex, saveIndex } = require('./indexStore'); const scanCartella = require('./scanCartella'); const postWithAuth = require('./postWithAuth'); const { WEB_ROOT, SEND_PHOTOS, BASE_URL, WRITE_INDEX, SUPPORTED_EXTS } = require('../config'); const createCleanupFunctions = require('./orphanCleanup'); // Variabile per log completo o ridotto const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true"); // Formatta ms β†’ HH:MM:SS function formatTime(ms) { const sec = Math.floor(ms / 1000); const h = String(Math.floor(sec / 3600)).padStart(2, '0'); const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0'); const s = String(sec % 60).padStart(2, '0'); return `${h}:${m}:${s}`; } // --------------------------------------------------------- // ⚑ CONTEGGIO FILE VELOCE (senza hashing, EXIF, sharp, ecc.) // --------------------------------------------------------- async function countFilesFast(rootDir) { let count = 0; async function walk(dir) { let entries = []; try { entries = await fsp.readdir(dir, { withFileTypes: true }); } catch { return; } for (const e of entries) { const abs = path.join(dir, e.name); if (e.isDirectory()) { await walk(abs); } else { const ext = path.extname(e.name).toLowerCase(); if (SUPPORTED_EXTS.has(ext)) { count++; } } } } await walk(rootDir); return count; } async function scanPhoto(dir, userName, db) { const start = Date.now(); log(`πŸ”΅ Inizio scan globale per user=${userName}`); const previousIndexTree = await loadPreviousIndex(); const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {})); const { buildIdsListForFolder, removeIdFromList, deleteThumbsById, deleteFromDB } = createCleanupFunctions(db); const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos'); const userDir = path.join(photosRoot, userName, 'original'); let totalNew = 0; let totalDeleted = 0; let totalUnchanged = 0; let newFiles = []; // --------------------------------------------------------- // 1) ⚑ CONTEGGIO FILE SUPER VELOCE // --------------------------------------------------------- const TOTAL_FILES = await countFilesFast(userDir); if (TOTAL_FILES === 0) { log(`Nessun file trovato per user=${userName}`); return []; } log(`πŸ“¦ File totali da processare: ${TOTAL_FILES}`); let CURRENT = 0; // --------------------------------------------------------- // Funzione per aggiornare il file statico leggibile dall’HTML // --------------------------------------------------------- async function updateStatusFile() { const now = Date.now(); const elapsedMs = now - start; const avg = elapsedMs / CURRENT; const remainingMs = (TOTAL_FILES - CURRENT) * avg; const status = { current: CURRENT, total: TOTAL_FILES, percent: Number((CURRENT / TOTAL_FILES * 100).toFixed(2)), eta: formatTime(remainingMs), elapsed: formatTime(elapsedMs) }; const statusPath = path.resolve(__dirname, "..", "..", "public/photos/scan_status.json"); await fsp.writeFile(statusPath, JSON.stringify(status)); } // --------------------------------------------------------- // 2) SCAN REALE DELLE CARTELLE // --------------------------------------------------------- async function scanSingleFolder(user, cartella, absCartella) { log(`πŸ“ Inizio cartella: ${cartella}`); let idsIndex = await buildIdsListForFolder(user, cartella); let newCount = 0; let unchangedCount = 0; let deletedCount = 0; for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) { CURRENT++; const fileName = m.path.split('/').pop(); const prefix = `[${CURRENT}/${TOTAL_FILES}]`; // Aggiorna file statico per l’HTML await updateStatusFile(); // βšͺ FILE INVARIATO if (m.unchanged) { if (LOG_VERBOSE) { log(`${prefix} βšͺ Invariato: ${fileName}`); } idsIndex = removeIdFromList(idsIndex, m.id); unchangedCount++; continue; } // 🟒 FILE NUOVO O MODIFICATO log(`${prefix} 🟒 Nuovo/Modificato: ${fileName}`); nextIndexTree[m.user] ??= {}; nextIndexTree[m.user][m.cartella] ??= {}; nextIndexTree[m.user][m.cartella][m.id] = { id: m.id, user: m.user, cartella: m.cartella, path: m.path, hash: m._indexHash }; idsIndex = removeIdFromList(idsIndex, m.id); newCount++; newFiles.push(m); } // πŸ”΄ ORFANI for (const orphanId of idsIndex) { const old = previousIndexTree?.[user]?.[cartella]?.[orphanId]; const fileName = old?.path?.split('/').pop() || orphanId; log(` πŸ”΄ Eliminato: ${fileName}`); await deleteThumbsById(orphanId); deleteFromDB(orphanId); const userTree = nextIndexTree[user]; if (userTree?.[cartella]?.[orphanId]) { delete userTree[cartella][orphanId]; } deletedCount++; } log(`πŸ“Š Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`); totalNew += newCount; totalDeleted += deletedCount; totalUnchanged += unchangedCount; } // --------------------------------------------------------- // SCAN SOTTOCARTELLE // --------------------------------------------------------- let entries = []; try { entries = await fsp.readdir(userDir, { withFileTypes: true }); } catch { log(`Nessuna directory per utente ${userName}`); return []; } for (const e of entries) { if (!e.isDirectory()) continue; const cartella = e.name; const absCartella = path.join(userDir, cartella); await scanSingleFolder(userName, cartella, absCartella); } // --------------------------------------------------------- // SALVO INDEX // --------------------------------------------------------- if (WRITE_INDEX) { await saveIndex(nextIndexTree); } // --------------------------------------------------------- // INVIO AL SERVER / POPOLAZIONE DB // --------------------------------------------------------- if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) { for (const p of newFiles) { const fileName = p.path.split('/').pop(); try { await postWithAuth(`${BASE_URL}/photos`, p); log(`πŸ“€ Inviato al server: ${fileName}`); } catch (err) { log(`Errore invio ${fileName}: ${err.message}`); } } } // --------------------------------------------------------- // FINE SCAN // --------------------------------------------------------- const elapsed = ((Date.now() - start) / 1000).toFixed(2); log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`); log(`⏱ Tempo totale: ${elapsed}s`); return newFiles; } module.exports = scanPhoto;