photo_server_json_flutter_c.../api_v1/scanner/scanPhoto.js
2026-03-23 12:31:01 +01:00

343 lines
9.7 KiB
JavaScript

// api_v1/scanner/scanPhoto.js
const path = require('path');
const fsp = require('fs/promises');
const { log } = require('./logger');
const scanCartella = require('./scanCartella');
const processFile = require('./processFile');
const postWithAuth = require('./postWithAuth');
const { sha256 } = require('./utils');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
SUPPORTED_EXTS
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
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}`;
}
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 { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let newFiles = [];
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;
async function updateStatusFile() {
const now = Date.now();
const elapsedMs = now - start;
const avg = elapsedMs / Math.max(CURRENT, 1);
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));
}
// ---------------------------------------------------------
// SCAN DI UNA SINGOLA CARTELLA
// ---------------------------------------------------------
async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`);
const rows = await db('photos')
.where({ user, cartella })
.select('id');
const idsSet = new Set(rows.map(r => r.id));
for await (const f of scanCartella(user, cartella, absCartella, db)) {
CURRENT++;
await updateStatusFile();
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
const fileName = f.name;
const id = f.id;
const st = f.stat;
let prev = await db("photos")
.select("id", "size_bytes", "mtimeMs", "_indexHash", "fast_hash", "path")
.where({ id })
.first();
if (prev && LOG_VERBOSE) {
log(`${prefix} ⚪ Invariato: ${fileName}`);
}
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
// ---------------------------------------------------------
// PATH CAMBIATO = NUOVO FILE
// ---------------------------------------------------------
if (prev && prev.path !== f.path) {
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
prev = null;
}
// ---------------------------------------------------------
// NUOVO FILE
// ---------------------------------------------------------
if (!prev) {
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
const meta = await processFile(
user,
cartella,
f.relPath,
f.absPath,
f.ext,
st
);
meta.id = id;
meta.path = f.path;
// INSERT CORRETTO (senza colonna gps)
await db("photos").insert({
id: meta.id,
user,
cartella,
name: meta.name,
path: meta.path,
thub1: meta.thub1,
thub2: meta.thub2,
mime_type: meta.mime_type,
width: meta.width,
height: meta.height,
rotation: meta.rotation,
size_bytes: meta.size_bytes,
mtimeMs: meta.mtimeMs,
duration_ms: meta.duration_ms,
taken_at: meta.taken_at,
data: meta.data,
lat: meta.gps?.lat ?? null,
lon: meta.gps?.lng ?? null,
alt: meta.gps?.alt ?? null,
location: meta.location ? JSON.stringify(meta.location) : null,
_indexHash: meta._indexHash,
fast_hash: fastHash
});
await db('photo_changes').insert({
photo_id: meta.id,
user,
change_type: 'added',
timestamp: new Date().toISOString()
});
idsSet.delete(id);
newFiles.push(meta);
//log(`🟢 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
continue;
}
// ---------------------------------------------------------
// FAST-SIZE-SKIP
// ---------------------------------------------------------
if (prev.size_bytes === st.size) {
//log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
idsSet.delete(id);
await db("photos")
.where({ id })
.update({
path: f.path,
size_bytes: st.size,
mtimeMs: st.mtimeMs,
fast_hash: fastHash
});
continue;
}
// ---------------------------------------------------------
// FAST-HASH-SKIP
// ---------------------------------------------------------
if (prev.fast_hash === fastHash) {
//log(`🔵 [FAST-HASH-SKIP] id=${id}`);
idsSet.delete(id);
await db("photos")
.where({ id })
.update({
path: f.path,
size_bytes: st.size,
mtimeMs: st.mtimeMs
});
continue;
}
// ---------------------------------------------------------
// MODIFICATO
// ---------------------------------------------------------
//log(`🟠 [FULL-SCAN] id=${id}`);
log(`${prefix} 🟠 Nuovo/Modificato: ${fileName}`);
const meta = await processFile(
user,
cartella,
f.relPath,
f.absPath,
f.ext,
st
);
meta.id = id;
meta.path = f.path;
await db("photos")
.insert({
id: meta.id,
user,
cartella,
name: meta.name,
path: meta.path,
thub1: meta.thub1,
thub2: meta.thub2,
mime_type: meta.mime_type,
width: meta.width,
height: meta.height,
rotation: meta.rotation,
size_bytes: meta.size_bytes,
mtimeMs: meta.mtimeMs,
duration_ms: meta.duration_ms,
taken_at: meta.taken_at,
data: meta.data,
lat: meta.gps?.lat ?? null,
lon: meta.gps?.lng ?? null,
alt: meta.gps?.alt ?? null,
location: meta.location ? JSON.stringify(meta.location) : null,
_indexHash: meta._indexHash,
fast_hash: fastHash
})
.onConflict("id")
.merge();
await db('photo_changes').insert({
photo_id: meta.id,
user,
change_type: 'updated',
timestamp: new Date().toISOString()
});
idsSet.delete(id);
newFiles.push(meta);
log(`${prefix} 🟠 Nuovo/Modificato al server ${fileName}`);
}
// ---------------------------------------------------------
// ORFANI
// ---------------------------------------------------------
for (const orphanId of idsSet) {
log(` 🔴 Cancellato ${orphanId}`);
await deleteThumbsById(orphanId);
await deleteFromDB(orphanId, user);
}
}
// ---------------------------------------------------------
// SCAN DI TUTTE LE CARTELLE
// ---------------------------------------------------------
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);
}
// ---------------------------------------------------------
// INVIO AL SERVER REMOTO
// ---------------------------------------------------------
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
log(`📤 [SEND START] newFiles=${newFiles.length}`);
for (const p of newFiles) {
try {
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📥 [SENT TO SERVER] ${p.name}`);
} catch (err) {
log(`❌ [SERVER SENT ERROR] ${p.name}${err.message}`);
}
}
} else {
log(`⚠️ [NO SEND] newFiles=${newFiles.length}`);
}
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO in ${elapsed}s`);
return newFiles;
}
module.exports = scanPhoto;