343 lines
9.7 KiB
JavaScript
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;
|