247 lines
7 KiB
JavaScript
247 lines
7 KiB
JavaScript
// 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;
|