modulare funzionante da verificare admin.html

This commit is contained in:
Fabio 2026-03-26 16:47:50 +01:00
parent 107f6c9609
commit 071f0b3e40
89 changed files with 27114 additions and 2429 deletions

11
.env
View file

@ -22,8 +22,17 @@ LOG_FILE=scan.log
LOG_DIR=public
LOG_VERBOSE=true
# POLLING_TIME in secondi
# Funzionamento:
# - POLLING_TIME = 0 → polling disattivato (solo WebSocket)
# - POLLING_TIME > 0 → esegue incrementalSync() ogni N secondi
GALLERY_REFRESH_SECONDS=30
# Perché esiste?
# - Serve come "rete di sicurezza" se il WebSocket cade,
# se la tab viene sospesa, o se qualche evento viene perso.
# - Google Photos, Dropbox e iCloud usano lo stesso approccio.
GALLERY_REFRESH_SECONDS=300
WS_PORT=4002
WS_HOST=0.0.0.0

View file

@ -1 +0,0 @@
lock

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

Binary file not shown.

View file

@ -0,0 +1,64 @@
// api_v1/scanner/debugVideoDates.js
const path = require('path');
const { probeVideo } = require('./video');
async function debugVideo(absPath) {
console.log("🎥 File:", absPath);
const info = await probeVideo(absPath);
console.log("\n=== RAW probeVideo(info) ===\n");
console.dir(info, { depth: 6 });
// Estrazione campi data come nello scanner
const formatTags = info.format?.tags || {};
const stream0Tags = info.streams?.[0]?.tags || {};
const stream1Tags = info.streams?.[1]?.tags || {};
const creationFormat = formatTags.creation_time || null;
const creationStream0 = stream0Tags.creation_time || null;
const creationStream1 = stream1Tags.creation_time || null;
console.log("\n=== DATE CANDIDATE ===");
console.log("format.tags.creation_time :", creationFormat);
console.log("streams[0].tags.creation_time:", creationStream0);
console.log("streams[1].tags.creation_time:", creationStream1);
// Simulazione logica attuale dello scanner (ma esplicita)
let videoDate =
creationFormat ||
creationStream0 ||
creationStream1 ||
null;
console.log("\nScelta videoDate (prima di qualsiasi conversione):", videoDate);
if (videoDate) {
// Variante 1: SENZA conversione (quella che ti ho suggerito)
const takenAtRaw = videoDate;
// Variante 2: CON conversione (quella che ti sballa il giorno)
const takenAtIso = new Date(videoDate).toISOString();
console.log("\n=== CONFRONTO ===");
console.log("takenAtRaw (usato così com'è) :", takenAtRaw);
console.log("takenAtIso (new Date().toISOString):", takenAtIso);
} else {
console.log("\n⚠ Nessuna data trovata nei tag video.");
}
}
async function main() {
const arg = process.argv[2];
if (!arg) {
console.error("Uso: node debugVideoDates.js /percorso/al/video.mp4");
process.exit(1);
}
const absPath = path.resolve(arg);
await debugVideo(absPath);
}
main().catch(err => {
console.error("❌ Errore debugVideoDates:", err);
});

View file

@ -0,0 +1,55 @@
// api_v1/scanner/deleteFolder.js
const path = require('path');
const fsp = require('fs/promises');
const { WEB_ROOT } = require('../config');
module.exports = function createDeleteFolderFunctions(db) {
const createCleanupFunctions = require('./orphanCleanup');
const { deleteFromDB } = createCleanupFunctions(db);
const recordChange = require('./recordChange');
/**
* Cancella una cartella per un utente:
* - elimina tutti i record DB
* - registra removed
* - elimina la cartella thumbs residua
*/
async function deleteFolderForUser(userName, folderName) {
// 1) Recupera tutti gli ID nel DB
const rows = await db('photos')
.where({ user: userName, cartella: folderName })
.select('id');
// 2) Cancella record DB + registra removed
for (const r of rows) {
const id = r.id;
await deleteFromDB(id, userName);
await recordChange(db, id, userName, "removed");
}
// 3) Cancella la cartella thumbs residua
const thumbsDir = path.join(
WEB_ROOT,
userName,
"thumbs",
folderName
);
try {
await fsp.rm(thumbsDir, { recursive: true, force: true });
console.log(`🧹 Rimossa cartella thumbs residua: ${thumbsDir}`);
} catch (err) {
console.log(`⚠️ Errore rimozione thumbs dir: ${thumbsDir}`, err);
}
return {
removedRecords: rows.length,
removedThumbsDir: thumbsDir
};
}
return { deleteFolderForUser };
};

View file

@ -1,22 +1,81 @@
// elevation.js
async function getElevation(lat, lon) {
console.log("OpenElevation request:", lat, lon);
// api_v1/scanner/elevation.js
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`;
// ---------------------------------------------------------
// Provider 1: OpenElevation
// ---------------------------------------------------------
async function tryOpenElevation(lat, lon) {
try {
const res = await fetch(url);
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`;
const res = await fetch(url, { timeout: 5000 });
if (!res.ok) {
console.log("OpenElevation status:", res.status);
return null;
}
const json = await res.json();
console.log("OpenElevation response:", json);
const text = await res.text();
return json?.results?.[0]?.elevation ?? null;
// Se non è JSON → errore HTML → fallback
if (!text.startsWith('{')) {
console.log("OpenElevation non-JSON:", text.slice(0, 60));
return null;
}
const data = JSON.parse(text);
return data?.results?.[0]?.elevation ?? null;
} catch (err) {
console.warn("Errore OpenElevation:", err);
console.log("Errore OpenElevation:", err.message);
return null;
}
}
module.exports = { getElevation };
// ---------------------------------------------------------
// Provider 2: OpenTopoData (fallback)
// ---------------------------------------------------------
async function tryOpenTopoData(lat, lon) {
try {
const url = `https://api.opentopodata.org/v1/eudem25m?locations=${lat},${lon}`;
const res = await fetch(url, { timeout: 5000 });
if (!res.ok) {
console.log("OpenTopoData status:", res.status);
return null;
}
const text = await res.text();
if (!text.startsWith('{')) {
console.log("OpenTopoData non-JSON:", text.slice(0, 60));
return null;
}
const data = JSON.parse(text);
return data?.results?.[0]?.elevation ?? null;
} catch (err) {
console.log("Errore OpenTopoData:", err.message);
return null;
}
}
// ---------------------------------------------------------
// Funzione principale con fallback
// ---------------------------------------------------------
async function getElevation(lat, lon) {
// 1⃣ Prova OpenElevation
const elev1 = await tryOpenElevation(lat, lon);
if (elev1 !== null) return elev1;
console.log("⚠️ OpenElevation fallito → uso OpenTopoData");
// 2⃣ Prova OpenTopoData
const elev2 = await tryOpenTopoData(lat, lon);
if (elev2 !== null) return elev2;
console.log("❌ Nessun provider di elevazione disponibile");
return null;
}
module.exports = getElevation;

View file

@ -0,0 +1,22 @@
// elevation.js
async function getElevation(lat, lon) {
console.log("OpenElevation request:", lat, lon);
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`;
try {
const res = await fetch(url);
console.log("OpenElevation status:", res.status);
const json = await res.json();
console.log("OpenElevation response:", json);
return json?.results?.[0]?.elevation ?? null;
} catch (err) {
console.warn("Errore OpenElevation:", err);
return null;
}
}
module.exports = { getElevation };

View file

@ -8,7 +8,8 @@ const { createVideoThumbnail, createThumbnails } = require('./thumbs');
const { probeVideo } = require('./video');
const loc = require('../geo.js');
const { WEB_ROOT, PATH_FULL } = require('../config');
const { getElevation } = require('./elevation');
//const { getElevation } = require('./elevation');
const getElevation = require('./elevation');
async function processFile(userName, cartella, fileRelPath, absPath, ext, st) {
@ -50,10 +51,33 @@ let takenAtIso = parseExifDateUtc(timeRaw);
// Fallback per i video
if (isVideo) {
const info = await probeVideo(absPath);
// Cerca la data nei punti standard
const creationFormat = info.format?.tags?.creation_time || null;
const creationStream0 = info.streams?.[0]?.tags?.creation_time || null;
const creationStream1 = info.streams?.[1]?.tags?.creation_time || null;
// Scegli la prima disponibile
const videoDate =
creationFormat ||
creationStream0 ||
creationStream1 ||
null;
if (videoDate) {
// NON convertire, NON usare new Date()
takenAtIso = videoDate;
timeRaw = videoDate;
} else {
// Fallback finale: mtime del file
const fallback = new Date(st.mtimeMs).toISOString();
takenAtIso = fallback;
timeRaw = fallback;
}
}
// --- GPS ---

View file

@ -0,0 +1,183 @@
const path = require('path');
const fsp = require('fs/promises');
const ExifReader = require('exifreader');
const sharp = require('sharp');
const { sha256, inferMimeFromExt, parseExifDateUtc } = require('./utils');
const { extractGpsFromExif, extractGpsWithExiftool } = require('./gps');
const { createVideoThumbnail, createThumbnails } = require('./thumbs');
const { probeVideo } = require('./video');
const loc = require('../geo.js');
const { WEB_ROOT, PATH_FULL } = require('../config');
//const { getElevation } = require('./elevation');
const getElevation = require('./elevation');
async function processFile(userName, cartella, fileRelPath, absPath, ext, st) {
const isVideo = ['.mp4', '.mov', '.m4v'].includes(ext);
const thumbBase = path.join(
WEB_ROOT,
'photos',
userName,
'thumbs',
cartella,
path.dirname(fileRelPath)
);
await fsp.mkdir(thumbBase, { recursive: true });
// --- Nome file + estensione (dal path ASSOLUTO, sempre corretto) ---
const parsed = path.parse(absPath);
const baseName = parsed.name; // IMG_0249
const extName = parsed.ext; // .JPG
const absThumbMin = path.join(thumbBase, `${baseName}_min.jpg`);
const absThumbAvg = path.join(thumbBase, `${baseName}_avg.jpg`);
if (isVideo) {
await createVideoThumbnail(absPath, absThumbMin, absThumbAvg);
} else {
await createThumbnails(absPath, absThumbMin, absThumbAvg);
}
// --- EXIF ---
let tags = {};
try {
tags = await ExifReader.load(absPath, { expanded: true });
} catch {}
let timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
let takenAtIso = parseExifDateUtc(timeRaw);
// Fallback per i video
if (isVideo) {
const fallback = new Date(st.mtimeMs).toISOString();
takenAtIso = fallback;
timeRaw = fallback;
}
// --- GPS ---
let gps = null;
if (isVideo) {
// i video usano exiftool
gps = await extractGpsWithExiftool(absPath);
} else {
// le foto usano exifreader
gps = extractGpsFromExif(tags);
}
// --- ALTITUDINE DA SERVIZIO ESTERNO SE MANCANTE ---
if (gps && gps.lat && gps.lng) {
if (gps.alt == null) {
gps.alt = await getElevation(gps.lat, gps.lng);
}
}
// --- DIMENSIONI & ROTAZIONE ---
let width = null, height = null, duration = null, duration_ms = null, rotation = 0;
if (isVideo) {
const info = await probeVideo(absPath);
const stream = info.streams?.find(s => s.width && s.height);
if (stream) {
width = stream.width;
height = stream.height;
rotation = 0;
if (stream?.tags?.rotate) {
rotation = Number(stream.tags.rotate);
}
const sdl = stream?.side_data_list;
if (sdl && Array.isArray(sdl)) {
const rotEntry = sdl.find(d => d.rotation !== undefined);
if (rotEntry) rotation = Number(rotEntry.rotation);
const matrixEntry = sdl.find(d => typeof d.displaymatrix === 'string');
if (matrixEntry) {
const match = matrixEntry.displaymatrix.match(/rotation of ([\-0-9]+) degrees/i);
if (match) rotation = Number(match[1]);
}
}
rotation = ((rotation % 360) + 360) % 360;
}
duration = info.format?.duration || null;
duration_ms = duration ? Math.round(duration * 1000) : null;
} else {
try {
const meta = await sharp(absPath).metadata();
width = meta.width || null;
height = meta.height || null;
} catch {}
try {
const raw =
tags?.exif?.Orientation?.value ??
tags?.image?.Orientation?.value ??
tags?.ifd0?.Orientation?.value ??
null;
const val = Array.isArray(raw) ? raw[0] : raw;
const map = { 1: 0, 3: 180, 6: 90, 8: 270 };
rotation = map[val] ?? 0;
} catch {}
}
const mime_type = inferMimeFromExt(ext);
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
const location = gps ? await loc(gps.lng, gps.lat) : null;
//
// --- GESTIONE PATH FULL / RELATIVI ---
//
const relPath = fileRelPath;
const relThub1 = fileRelPath.replace(/\.[^.]+$/, '_min.jpg');
const relThub2 = fileRelPath.replace(/\.[^.]+$/, '_avg.jpg');
const fullPath = PATH_FULL
? path.posix.join('/photos', userName, cartella, fileRelPath)
: relPath;
const fullThub1 = PATH_FULL
? path.posix.join('/photos', userName, 'thumbs', cartella, relThub1)
: relThub1;
const fullThub2 = PATH_FULL
? path.posix.join('/photos', userName, 'thumbs', cartella, relThub2)
: relThub2;
return {
id,
user: userName,
cartella,
name: baseName + extName, // 👈 SEMPRE CORRETTO
path: fullPath,
thub1: fullThub1,
thub2: fullThub2,
gps,
data: timeRaw,
taken_at: takenAtIso,
mime_type,
width,
height,
rotation,
size_bytes: st.size,
mtimeMs: st.mtimeMs,
duration_ms: isVideo ? duration_ms : null,
location
};
}
module.exports = processFile;

View file

@ -0,0 +1,29 @@
// api_v1/scanner/recordChange.js
const { log } = require('./logger');
async function recordChange(db, photo_id, user, change_type) {
const timestamp = new Date().toISOString();
// 🔒 deduplica "removed" per stessa foto/utente
if (change_type === 'removed') {
const already = await db('photo_changes')
.where({ photo_id, user, change_type: 'removed' })
.first();
if (already) {
log(`📘 [CHANGES] removed già registrato → ${photo_id}, skip`);
return;
}
}
await db("photo_changes").insert({
photo_id,
user,
change_type,
timestamp
});
log(`📘 [CHANGES] ${change_type}${photo_id}`);
}
module.exports = recordChange;

210
api_v1/scanner/scanAuto.js Normal file
View file

@ -0,0 +1,210 @@
const path = require('path');
const fsp = require('fs/promises');
const scanCartella = require('./scanCartella');
const processFile = require('./processFile');
const { sha256 } = require('./utils');
const createCleanupFunctions = require('./orphanCleanup');
const { WEB_ROOT } = require('../config');
const { log } = require('./logger');
// ---------------------------------------------------------
// NORMALIZZAZIONE PATH (toglie /app/...)
// ---------------------------------------------------------
function normalizeRelPath(user, dirPath, fileName) {
if (!dirPath) return "";
let rel = dirPath.replace(/\\/g, "/");
// Rimuove tutto fino a /photos/
rel = rel.replace(/^.*\/photos\//, "");
// Ora rel = "Fabio/original/2017Irlanda/.../"
const prefix = `${user}/original/`;
if (rel.startsWith(prefix)) {
rel = rel.slice(prefix.length);
}
if (rel.startsWith("/")) rel = rel.slice(1);
// Aggiunge il file
if (fileName) {
rel = `${rel}/${fileName}`.replace(/\/+/g, "/");
}
return rel; // es: "2017Irlanda19-29ago/IMG_0120.JPG"
}
// ---------------------------------------------------------
// CALCOLO ID IDENTICO AL VECCHIO SCAN
// ---------------------------------------------------------
function computeId(user, relPath) {
const parts = relPath.split('/').filter(Boolean);
const cartella = parts[0];
const fileRelPath = parts.slice(1).join('/');
const id = sha256(`${user}/${cartella}/${fileRelPath}`);
return { id, cartella, fileRelPath };
}
// ---------------------------------------------------------
// 1) ADD FILE
// ---------------------------------------------------------
async function handleAddFile(user, dirPath, fileName, db) {
const relPath = normalizeRelPath(user, dirPath, fileName);
log(`🟦 [ADD] user=${user} file=${relPath}`);
// 🔥 Ricostruzione identica al vecchio scanPhoto
const parts = relPath.split('/');
const cartella = parts[0]; // es: "2017Irlanda19-29ago"
const fileRelPath = parts.slice(1).join('/'); // es: "IMG_0116.JPG"
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const absPath = path.join(photosRoot, user, 'original', cartella, fileRelPath);
const ext = path.extname(fileName).toLowerCase();
const st = await fsp.stat(absPath);
// 🔥 FIX: processFile deve ricevere fileRelPath completo di "original/"
const meta = await processFile(
user,
cartella,
path.posix.join('original', cartella, fileRelPath),
absPath,
ext,
st
);
// Inserimento identico al vecchio scanPhoto
const row = {
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 ?? null,
taken_at: meta.taken_at ?? null,
data: meta.data ?? null,
lat: meta.gps?.lat ?? null,
lon: meta.gps?.lng ?? null,
alt: meta.gps?.alt ?? null,
location: meta.location ? JSON.stringify(meta.location) : null
};
await db('photos')
.insert(row)
.onConflict('id')
.merge();
log(`🟢 [ADD] Inserito/aggiornato id=${meta.id}`);
return row;
}
// ---------------------------------------------------------
// 2) DELETE FILE
// ---------------------------------------------------------
async function handleDeleteFile(user, dirPath, fileName, db) {
const relPath = normalizeRelPath(user, dirPath, fileName);
log(`🟥 [DEL] user=${user} file=${relPath}`);
const { id } = computeId(user, relPath);
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
await deleteThumbsById(id);
await deleteFromDB(id, user);
log(`🔴 [DEL] File eliminato id=${id}`);
return { deleted: id };
}
// ---------------------------------------------------------
// 3) ADD DIRECTORY
// ---------------------------------------------------------
async function handleAddDir(user, dir, db) {
log(`🟦 [ADD_DIR] user=${user} dir=${dir}`);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const absDir = path.join(photosRoot, user, 'original', dir);
log(`📁 [ADD_DIR] absDir=${absDir}`);
let results = [];
for await (const f of scanCartella(user, dir, absDir, db)) {
log(`📄 [ADD_DIR] File trovato: ${f.relPath}`);
const meta = await processFile(
user,
dir,
f.relPath,
f.absPath,
f.ext,
f.stat
);
await db('photos').insert(meta).onConflict('id').merge();
log(`🟢 [ADD_DIR] Inserito/aggiornato id=${meta.id} name=${meta.name}`);
results.push(meta);
}
log(`🟣 [ADD_DIR] Completato. Files=${results.length}`);
return results;
}
// ---------------------------------------------------------
// 4) DELETE DIRECTORY
// ---------------------------------------------------------
async function handleDeleteDir(user, dir, db) {
log(`🟥 [DEL_DIR] user=${user} dir=${dir}`);
const rows = await db('photos')
.where({ user, cartella: dir })
.select('id');
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
log(`📁 [DEL_DIR] Trovati ${rows.length} file da eliminare`);
for (const r of rows) {
log(`🗑️ [DEL_DIR] Eliminazione id=${r.id}`);
await deleteThumbsById(r.id);
await deleteFromDB(r.id, user);
}
log(`🔴 [DEL_DIR] Cartella eliminata. Totale file=${rows.length}`);
return { deleted: rows.length };
}
module.exports = {
handleAddFile,
handleDeleteFile,
handleAddDir,
handleDeleteDir
};

View file

@ -1,19 +1,11 @@
// api_v1/scanner/scanCartella.js
const path = require('path');
const fsp = require('fs/promises');
const fs = require('fs');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
const { log } = require('./logger');
/**
* scanCartella: genera informazioni sui file nella cartella
* Non esegue processFile scrive sul DB: lascia la logica di confronto a scanPhoto.
*
* Parametri:
* - userName
* - cartella
* - absCartella
* - db (opzionale, mantenuto per compatibilità)
*/
async function* scanCartella(userName, cartella, absCartella, db) {
async function* walk(currentAbs, relPath = '') {
@ -37,12 +29,17 @@ async function* scanCartella(userName, cartella, absCartella, db) {
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
// ID deterministico (stesso criterio di prima)
// ID deterministico basato sul percorso (come loriginale)
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
// Stat veloce
let st;
try { st = await fsp.stat(absPath); } catch { continue; }
try {
st = await fsp.stat(absPath);
} catch {
continue;
}
log(`📂 scanCartella → user=${userName} cartella=${cartella} relPath=${fileRelPath} id=${id}`);
yield {
id,

233
api_v1/scanner/scanFile.js Normal file
View file

@ -0,0 +1,233 @@
// api_v1/scanner/scanFile.js
const path = require('path');
const fsp = require('fs/promises');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
const { log } = require('./logger');
/**
* scanFile: genera informazioni su UN singolo file
* Restituisce lo stesso formato di scanCartella, ma senza recursion.
*
* Parametri:
* - userName
* - cartella
* - absFile (percorso assoluto del file)
* - db (opzionale)
*/
async function scanFileEntry(userName, cartella, absFile, db) {
const ext = path.extname(absFile).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) {
log(`⛔ Estensione non supportata: ${ext}`);
return null;
}
const name = path.basename(absFile);
const relPath = name; // singolo file → niente struttura ricorsiva
let st;
try {
st = await fsp.stat(absFile);
} catch {
log(`⛔ Impossibile leggere stat per ${absFile}`);
return null;
}
const id = sha256(`${userName}/${cartella}/${relPath}`);
log(`📄 scanFile → user=${userName} cartella=${cartella} file=${name} id=${id}`);
return {
id,
user: userName,
cartella,
name,
relPath,
absPath: absFile,
ext,
stat: st,
path: `/photos/${userName}/original/${cartella}/${relPath}`
};
}
async function scanFile(userName, cartella, absFile, db) {
const f = await scanFile(user, cart, absFile);
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()
});
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}`);
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}`);
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()
});
newFiles.push(meta);
log(`${prefix} 🟠 Nuovo/Modificato al server ${fileName}`);
}
}
}
module.exports = scanFile;

View file

@ -0,0 +1,53 @@
// api_v1/scanner/scanFile.js
const path = require('path');
const fsp = require('fs/promises');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
const { log } = require('./logger');
/**
* scanFile: genera informazioni su UN singolo file
* Restituisce lo stesso formato di scanCartella, ma senza recursion.
*
* Parametri:
* - userName
* - cartella
* - absFile (percorso assoluto del file)
* - db (opzionale)
*/
async function scanFile(userName, cartella, absFile, db) {
const ext = path.extname(absFile).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) {
log(`⛔ Estensione non supportata: ${ext}`);
return null;
}
const name = path.basename(absFile);
const relPath = name; // singolo file → niente struttura ricorsiva
let st;
try {
st = await fsp.stat(absFile);
} catch {
log(`⛔ Impossibile leggere stat per ${absFile}`);
return null;
}
const id = sha256(`${userName}/${cartella}/${relPath}`);
log(`📄 scanFile → user=${userName} cartella=${cartella} file=${name} id=${id}`);
return {
id,
user: userName,
cartella,
name,
relPath,
absPath: absFile,
ext,
stat: st,
path: `/photos/${userName}/original/${cartella}/${relPath}`
};
}
module.exports = scanFile;

View file

@ -0,0 +1,55 @@
// api_v1/scanner/scanFileEntry.js
const path = require('path');
const fsp = require('fs/promises');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
const { log } = require('./logger');
/**
* scanFile: genera informazioni su UN singolo file
* Restituisce lo stesso formato di scanCartella, ma senza recursion.
*
* Parametri:
* - userName
* - cartella
* - absFile (percorso assoluto del file)
* - db (opzionale)
*/
async function scanFile(userName, cartella, absFile, db) {
const ext = path.extname(absFile).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) {
log(`⛔ Estensione non supportata: ${ext}`);
return null;
}
const name = path.basename(absFile);
const relPath = name; // singolo file → niente struttura ricorsiva
// ID deterministico (funziona anche se il file non esiste)
const id = sha256(`${userName}/${cartella}/${relPath}`);
let st = null;
try {
st = await fsp.stat(absFile);
} catch {
// File NON esiste → caso DEL
log(`⛔ Impossibile leggere stat per ${absFile} (file mancante)`);
// Continuiamo comunque: l'ID è valido e serve per DEL
}
log(`📄 scanFile → user=${userName} cartella=${cartella} file=${name} id=${id}`);
return {
id,
user: userName,
cartella,
name,
relPath,
absPath: absFile,
ext,
stat: st, // può essere null
path: `/photos/${userName}/original/${cartella}/${relPath}`
};
}
module.exports = scanFile;

View file

@ -0,0 +1,75 @@
// api_v1/scanner/scanNewCartella.js
const path = require('path');
const fsp = require('fs/promises');
const { WEB_ROOT, SUPPORTED_EXTS } = require('../config');
const scanPhoto = require('./scanPhoto');
// ---------------------------------------------------------
// Conta ricorsivamente i file in /photos/<user>/original/<folder>/
// ---------------------------------------------------------
async function countFilesInUserFolder(userName, folderName) {
const rootDir = path.join(WEB_ROOT, userName, "original", folderName);
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;
}
// ---------------------------------------------------------
// Crea thumbs/<folder> e scansiona la cartella come fa /scan
// ---------------------------------------------------------
async function scanNewCartella(userName, folderName, db) {
// 1) crea la cartella root in thumbs
const thumbsDir = path.join(WEB_ROOT, userName, "thumbs", folderName);
await fsp.mkdir(thumbsDir, { recursive: true });
// 2) conta i file ricorsivamente
const TOTAL_FILES = await countFilesInUserFolder(userName, folderName);
// 3) esegui lo scan della cartella
const CURRENT = { value: 0 };
const start = Date.now();
const newFiles = await scanPhoto(
folderName,
userName,
db,
CURRENT,
TOTAL_FILES,
start
);
return {
ok: true,
folder: folderName,
totalFiles: TOTAL_FILES,
newFiles
};
}
module.exports = {
scanNewCartella,
countFilesInUserFolder
};

View file

@ -3,22 +3,19 @@ const path = require('path');
const fsp = require('fs/promises');
const { log } = require('./logger');
const scanCartella = require('./scanCartella');
const processFile = require('./processFile');
const scanPhotoCartella = require('./scanPhotoCartella');
const postWithAuth = require('./postWithAuth');
const { sha256 } = require('./utils');
const createCleanupFunctions = require('./orphanCleanup');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
SUPPORTED_EXTS
BASE_URL
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
// ---------------------------------------------------------
// FORMAT TIME
// ---------------------------------------------------------
function formatTime(ms) {
const sec = Math.floor(ms / 1000);
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
@ -27,66 +24,19 @@ function formatTime(ms) {
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() {
// ---------------------------------------------------------
// UPDATE STATUS FILE
// ---------------------------------------------------------
async function updateStatusFile(CURRENT, TOTAL_FILES, start) {
const now = Date.now();
const elapsedMs = now - start;
const avg = elapsedMs / Math.max(CURRENT, 1);
const remainingMs = (TOTAL_FILES - CURRENT) * avg;
const avg = elapsedMs / Math.max(CURRENT.value, 1);
const remainingMs = (TOTAL_FILES - CURRENT.value) * avg;
const status = {
current: CURRENT,
current: CURRENT.value,
total: TOTAL_FILES,
percent: Number((CURRENT / TOTAL_FILES * 100).toFixed(2)),
percent: Number((CURRENT.value / TOTAL_FILES * 100).toFixed(2)),
eta: formatTime(remainingMs),
elapsed: formatTime(elapsedMs)
};
@ -96,224 +46,34 @@ async function scanPhoto(dir, userName, db) {
}
// ---------------------------------------------------------
// SCAN DI UNA SINGOLA CARTELLA
// SCAN UNA SOLA CARTELLA
// ---------------------------------------------------------
async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`);
async function scanPhoto(dir, userName, db, CURRENT, TOTAL_FILES, start) {
const newFiles = [];
const rows = await db('photos')
.where({ user, cartella })
.select('id');
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
const idsSet = new Set(rows.map(r => r.id));
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
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 cartella = dir;
const absCartella = path.join(userDir, cartella);
await scanSingleFolder(userName, cartella, absCartella);
}
log(`📁 [SCAN CARTELLA] ${cartella}`);
await scanPhotoCartella(
db,
userName,
cartella,
absCartella,
newFiles,
updateStatusFile,
CURRENT,
TOTAL_FILES,
start,
deleteThumbsById,
deleteFromDB
);
// ---------------------------------------------------------
// INVIO AL SERVER REMOTO
@ -322,8 +82,8 @@ async function scanPhoto(dir, userName, db) {
log(`📤 [SEND START] newFiles=${newFiles.length}`);
for (const p of newFiles) {
try {
p.user = userName;
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📥 [SENT TO SERVER] ${p.name}`);
} catch (err) {
@ -334,9 +94,6 @@ async function scanPhoto(dir, userName, db) {
log(`⚠️ [NO SEND] newFiles=${newFiles.length}`);
}
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO in ${elapsed}s`);
return newFiles;
}

View file

@ -1,349 +0,0 @@
// 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 fileName = f.name;
const id = f.id;
const st = f.stat;
log(`🔍 [SCAN] File: ${fileName} id=${id} size=${st.size} mtime=${st.mtimeMs}`);
let prev = await db("photos")
.select("id", "size_bytes", "mtimeMs", "_indexHash", "fast_hash", "path")
.where({ id })
.first();
if (prev) {
log(` ↳ [PREV FOUND] size=${prev.size_bytes} mtime=${prev.mtimeMs} path=${prev.path}`);
} else {
log(` ↳ [PREV NOT FOUND]`);
}
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
// ---------------------------------------------------------
// PATH CAMBIATO = NUOVO FILE
// ---------------------------------------------------------
if (prev && prev.path !== f.path) {
log(`🟡 [PATH CHANGED] id=${id} old=${prev.path} new=${f.path}`);
prev = null;
}
// ---------------------------------------------------------
// NUOVO FILE
// ---------------------------------------------------------
if (!prev) {
log(`🟢 [NEW FILE DETECTED] id=${id} name=${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}`);
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(`🟠 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
}
// ---------------------------------------------------------
// ORFANI
// ---------------------------------------------------------
for (const orphanId of idsSet) {
log(`🔴 [ORPHAN DELETE] id=${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) {
log(`📤 [SEND] id=${p.id} name=${p.name} path=${p.path}`);
try {
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📥 [SERVER RESPONSE OK] id=${p.id}`);
} catch (err) {
log(`❌ [SERVER ERROR] id=${p.id} → ${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;

View file

@ -0,0 +1,57 @@
// api_v1/scanner/scanPhotoCartella.js
const scanCartella = require('./scanCartella');
const scanPhotoSingle = require('./scanPhotoSingle');
const { log } = require('./logger');
const recordChange = require('./recordChange'); // 👈 AGGIUNTO
async function scanPhotoCartella(
db,
user,
cartella,
absCartella,
newFiles,
updateStatusFile,
CURRENT,
TOTAL_FILES,
start,
deleteThumbsById,
deleteFromDB
) {
log(`📁 Scan cartella: ${cartella}`);
// Recupera gli ID già presenti nel DB
const rows = await db('photos')
.where({ user, cartella })
.select('id');
const idsSet = new Set(rows.map(r => r.id));
// Scansiona i file della cartella
for await (const f of scanCartella(user, cartella, absCartella, db)) {
// Aggiorna il contatore
CURRENT.value++;
// Aggiorna stato avanzamento
await updateStatusFile(CURRENT, TOTAL_FILES, start);
// Processa il file
const meta = await scanPhotoSingle(db, user, cartella, f, newFiles);
// Anche se identico, NON è orfano
idsSet.delete(f.id);
}
// Gestione orfani
for (const orphanId of idsSet) {
log(`🔴 Orfano: ${orphanId}`);
await deleteThumbsById(orphanId);
await deleteFromDB(orphanId, user);
// 👇 REGISTRAZIONE MODIFICA
await recordChange(db, orphanId, user, "removed");
}
}
module.exports = scanPhotoCartella;

View file

@ -0,0 +1,141 @@
// api_v1/scanner/scanPhotoSingle.js
const processFile = require('./processFile');
const { sha256 } = require('./utils');
const { log } = require('./logger');
const recordChange = require('./recordChange'); // 👈 AGGIUNTO
async function scanPhotoSingle(db, user, cartella, f, newFiles, prefix = '') {
const fileName = f.name;
const id = f.id;
const st = f.stat;
// Recupero precedente SENZA contentHash
let prev = await db("photos")
.select("id", "size_bytes", "mtimeMs", "fast_hash", "path")
.where({ id })
.first();
// Hash veloce basato su size + mtime
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
// ---------------------------------------------------------
// PATH CAMBIATO = NUOVO FILE
// ---------------------------------------------------------
if (prev && prev.path !== f.path) {
log(`${prefix} 🟢 Nuovo/Modificato (path changed): ${fileName}`);
prev = null;
}
// ---------------------------------------------------------
// NUOVO FILE
// ---------------------------------------------------------
if (!prev) {
log(`${prefix} 🟢 Nuovo: ${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,
fast_hash: fastHash
});
// 👇 REGISTRAZIONE MODIFICA
await recordChange(db, id, user, "added");
newFiles.push(meta);
return meta;
}
// ---------------------------------------------------------
// IDENTICO (fastHash uguale)
// ---------------------------------------------------------
if (prev.fast_hash === fastHash) {
await db("photos")
.where({ id })
.update({
path: f.path,
size_bytes: st.size,
mtimeMs: st.mtimeMs,
fast_hash: fastHash
});
return null;
}
// ---------------------------------------------------------
// MODIFICATO (fastHash diverso)
// ---------------------------------------------------------
log(`${prefix} 🟠 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")
.where({ id })
.update({
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,
fast_hash: fastHash
});
// 👇 REGISTRAZIONE MODIFICA
await recordChange(db, id, user, "updated");
newFiles.push(meta);
return meta;
}
module.exports = scanPhotoSingle;

View file

@ -0,0 +1,110 @@
// api_v1/scanner/scanPhotosUser.js
const path = require('path');
const fsp = require('fs/promises');
const scanPhoto = require('./scanPhoto');
const { log } = require('./logger');
const { WEB_ROOT, SUPPORTED_EXTS } = require('../config');
// ---------------------------------------------------------
// PRIMA PASSATA: conta TUTTI i file reali (ricorsiva)
// ---------------------------------------------------------
async function countFilesUser(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;
}
// ---------------------------------------------------------
// SECONDA PASSATA: scansiona SOLO le cartelle vere
// ---------------------------------------------------------
async function scanPhotosUser(userName, db) {
log(`🔵 Inizio scan TUTTE le cartelle per user=${userName}`);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let entries = [];
try {
entries = await fsp.readdir(userDir, { withFileTypes: true });
} catch {
log(`❌ Nessuna directory per utente ${userName}`);
return [];
}
// 🔥 Filtra SOLO cartelle vere dentro "original"
const folders = entries.filter(e => e.isDirectory()).map(e => e.name);
// ---------------------------------------------------------
// 🔥 RIMOZIONE CARTELLE CANCELLATE DAL FILESYSTEM
// ---------------------------------------------------------
const createDeleteFolderFunctions = require('./deleteFolder');
const { deleteFolderForUser } = createDeleteFolderFunctions(db);
const dbFolders = await db('photos')
.where({ user: userName })
.distinct('cartella')
.pluck('cartella');
for (const dbFolder of dbFolders) {
if (!folders.includes(dbFolder)) {
log(`🗑️ Cartella rimossa dal filesystem: ${dbFolder}`);
await deleteFolderForUser(userName, dbFolder);
}
}
// 🔥 PRIMA PASSATA: conta i file reali
const TOTAL_FILES = await countFilesUser(userDir);
const CURRENT = { value: 0 };
const start = Date.now();
log(`📊 File totali da scansionare: ${TOTAL_FILES}`);
const allNewFiles = [];
// 🔥 SECONDA PASSATA: UNA SOLA SCANSIONE PER CARTELLA
for (const cartella of folders) {
log(`📁 Scan cartella utente: ${cartella}`);
const newFiles = await scanPhoto(
cartella,
userName,
db,
CURRENT,
TOTAL_FILES,
start
);
if (newFiles?.length) {
allNewFiles.push(...newFiles);
}
}
log(`🟣 Scan COMPLETATO per user=${userName}. Nuovi file totali: ${allNewFiles.length}`);
return allNewFiles;
}
module.exports = scanPhotosUser;

View file

@ -2,61 +2,39 @@
const path = require('path');
const fsp = require('fs/promises');
const scanCartella = require('./scanCartella');
const processFile = require('./processFile');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
async function scanUserRoot(userName, userDir, previousIndexTree) {
console.log(`\n🔵 Inizio scan user: ${userName}`);
const results = [];
const entries = await fsp.readdir(userDir, { withFileTypes: true });
// ROOT FILES
for (const e of entries) {
if (e.isDirectory()) continue;
// 🔥 SCANSIONA SOLO LA CARTELLA "original"
const originalDir = path.join(userDir, "original");
const ext = path.extname(e.name).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) continue;
const absPath = path.join(userDir, e.name);
const st = await fsp.stat(absPath);
const fileRelPath = e.name;
const cartella = '_root';
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
const hash = sha256(`${st.size}-${st.mtimeMs}`);
const prev = previousIndexTree?.[userName]?.[cartella]?.[id];
if (prev && prev.hash === hash) continue;
const meta = await processFile(
userName,
cartella,
fileRelPath,
absPath,
ext,
st
);
meta.id = id;
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
meta._indexHash = hash;
results.push(meta);
let entries;
try {
entries = await fsp.readdir(originalDir, { withFileTypes: true });
} catch (err) {
console.error(`❌ Errore lettura originalDir: ${originalDir}`, err);
return results;
}
// SUBFOLDERS
// 🔥 SCANSIONA SOLO LE SOTTOCARTELLE DI "original"
for (const e of entries) {
if (!e.isDirectory()) continue;
const cartella = e.name;
const absCartella = path.join(userDir, cartella);
const absCartella = path.join(originalDir, cartella);
console.log(` 📁 Cartella: ${cartella}`);
const files = await scanCartella(userName, cartella, absCartella, previousIndexTree);
const files = await scanCartella(
userName,
cartella,
absCartella,
previousIndexTree
);
results.push(...files);
}

View file

@ -2,6 +2,7 @@
const db = require('./knex');
async function init() {
// -------------------------------
// 1) Tabella PHOTOS
// -------------------------------
@ -37,7 +38,7 @@ async function init() {
t.string('fast_hash');
});
console.log("✔ Tabella 'photos' creata correttamente (con _indexHash + fast_hash)");
console.log("✔ Tabella 'photos' creata correttamente (senza contentHash)");
} else {
console.log("✔ Tabella 'photos' già esistente");
@ -59,6 +60,9 @@ async function init() {
});
console.log("✔ Colonna 'fast_hash' aggiunta");
}
// ❌ NON aggiungiamo più contentHash
// Se esiste già, non dà problemi.
}
// -------------------------------
@ -76,6 +80,7 @@ async function init() {
});
console.log("✔ Tabella 'photo_changes' creata correttamente");
} else {
console.log("✔ Tabella 'photo_changes' già esistente");
}

View file

@ -2,9 +2,12 @@
const db = require('./knex');
async function init() {
const exists = await db.schema.hasTable('photos');
// -------------------------------
// 1) Tabella PHOTOS
// -------------------------------
const existsPhotos = await db.schema.hasTable('photos');
if (!exists) {
if (!existsPhotos) {
await db.schema.createTable('photos', (t) => {
t.string('id').primary();
t.string('user');
@ -27,13 +30,54 @@ async function init() {
t.float('alt');
t.text('location'); // JSON string
// 👉 NUOVO: hash identico al vecchio index.json
// Hash lento (metadati)
t.string('_indexHash');
// Hash veloce (size-mtime)
t.string('fast_hash');
});
console.log("✔ Tabella 'photos' creata correttamente (con _indexHash)");
console.log("✔ Tabella 'photos' creata correttamente (con _indexHash + fast_hash)");
} else {
console.log("✔ Tabella 'photos' già esistente");
// Controllo colonna _indexHash
const hasIndexHash = await db.schema.hasColumn('photos', '_indexHash');
if (!hasIndexHash) {
await db.schema.alterTable('photos', (t) => {
t.string('_indexHash');
});
console.log("✔ Colonna '_indexHash' aggiunta");
}
// Controllo colonna fast_hash
const hasFastHash = await db.schema.hasColumn('photos', 'fast_hash');
if (!hasFastHash) {
await db.schema.alterTable('photos', (t) => {
t.string('fast_hash');
});
console.log("✔ Colonna 'fast_hash' aggiunta");
}
}
// -------------------------------
// 2) Tabella PHOTO_CHANGES
// -------------------------------
const existsChanges = await db.schema.hasTable('photo_changes');
if (!existsChanges) {
await db.schema.createTable('photo_changes', (t) => {
t.increments('id').primary();
t.string('photo_id');
t.string('user');
t.string('change_type'); // added | updated | removed
t.string('timestamp'); // ISO string
});
console.log("✔ Tabella 'photo_changes' creata correttamente");
} else {
console.log("✔ Tabella 'photo_changes' già esistente");
}
process.exit();

10
f.sh
View file

@ -1,10 +0,0 @@
#!/bin/bash
WATCH_DIR="./public/photos/Fabio/original"
echo "Monitoro: $WATCH_DIR"
inotifywait -m -r -e create,modify,delete "$WATCH_DIR" |
while read path action file; do
echo "modificato Fabio → $action su $file"
done

27
f1.sh
View file

@ -1,27 +0,0 @@
#!/bin/bash
WATCH_DIR="./public/photos/Fabio/original"
DELAY=10 # secondi di quiete richiesti
TIMER_PID=""
echo "Monitoro: $WATCH_DIR"
# Funzione che parte dopo 10 secondi di quiete
run_after_delay() {
sleep $DELAY
echo "modificato Fabio (nessuna attività per $DELAY secondi)"
}
inotifywait -m -r -e create,modify,delete "$WATCH_DIR" |
while read path action file; do
echo "Evento rilevato: $action su $file"
# Se esiste un timer precedente, lo uccidiamo
if [[ -n "$TIMER_PID" ]]; then
kill "$TIMER_PID" 2>/dev/null
fi
# Avviamo un nuovo timer in background
run_after_delay &
TIMER_PID=$!
done

53
f2.sh
View file

@ -1,53 +0,0 @@
#!/bin/bash
WATCH_DIR="./public/photos/Fabio/original"
DELAY=10
LOCK_FILE="./Fabio.scan"
echo "Monitoro: $WATCH_DIR"
LAST_EVENT_TIME=0
# Thread che controlla l'inattività
check_inactivity() {
while true; do
sleep 1
# Se non è mai arrivato un evento, continua ad aspettare
if (( LAST_EVENT_TIME == 0 )); then
continue
fi
NOW=$(date +%s)
DIFF=$(( NOW - LAST_EVENT_TIME ))
# Se non sono passati 10 secondi → continua ad aspettare
if (( DIFF < DELAY )); then
continue
fi
# Se esiste il lock file → aspetta altri 10 secondi
if [[ -f "$LOCK_FILE" ]]; then
echo "Lock presente ($LOCK_FILE), attendo altri $DELAY secondi..."
sleep $DELAY
continue
fi
# QUI: 10 secondi di quiete + lock libero → esegui
echo "modificato Fabio (nessuna attività per $DELAY secondi)"
touch "$LOCK_FILE"
# Resetta il timer per evitare ripetizioni
LAST_EVENT_TIME=0
done
}
# Avvia il thread di controllo inattività
check_inactivity &
# Listener degli eventi
inotifywait -m -r -e create,modify,delete "$WATCH_DIR" |
while read path action file; do
echo "Evento rilevato: $action su $file"
LAST_EVENT_TIME=$(date +%s)
done

40
find-esm.js Normal file
View file

@ -0,0 +1,40 @@
const fs = require("fs");
const path = require("path");
function scan(dir) {
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const full = path.join(dir, item.name);
if (item.isDirectory()) {
scan(full);
continue;
}
// 1) File .mjs → ESM sicuro
if (full.endsWith(".mjs")) {
console.log("⚠️ FILE .mjs (ESM):", full);
continue;
}
// 2) File .js → controlliamo se contiene import/export
if (full.endsWith(".js")) {
const content = fs.readFileSync(full, "utf8");
if (/^\s*import\s/m.test(content)) {
console.log("⚠️ IMPORT trovato:", full);
}
if (/^\s*export\s/m.test(content)) {
console.log("⚠️ EXPORT trovato:", full);
}
if (/import\.meta\.url/.test(content)) {
console.log("⚠️ import.meta.url trovato:", full);
}
}
}
}
console.log("🔍 Scansione in corso...");
scan(process.cwd());
console.log("✔️ Scansione completata.");

View file

@ -1,4 +1,8 @@
{
"type": "commonjs",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"axios": "^1.13.6",
"bcrypt": "^6.0.0",

View file

@ -45,7 +45,7 @@
<button onclick="scan()">Scansiona Foto</button>
<button onclick="resetDB()">Reset DB</button>
<button onclick="readDB()">Leggi DB</button>
<button onclick="readDBUser()">Leggi DB</button>
<button onclick="deletePhoto()">Cancella Foto per ID</button>
<button onclick="findIdIndex()">Cerca ID in index.json</button>
<button onclick="resetDBuser()">Reset DB Utente</button>
@ -180,7 +180,7 @@ async function loadConfig() {
BASE_URL = cfg.baseUrl;
}
async function readDB() {
async function readDB1() {
const res = await fetch(`${BASE_URL}/photos`, {
headers: { "Authorization": "Bearer " + token }
});
@ -189,6 +189,42 @@ async function readDB() {
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
}
async function readDBUser() {
const payload = parseJwt(token);
const user = payload.name;
const res = await fetch(`${BASE_URL}/photos?user=${encodeURIComponent(user)}`, {
headers: { "Authorization": "Bearer " + token }
});
if (!res.ok) {
console.error("Errore readDB:", res.status);
return;
}
db = await res.json();
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
}
async function readDB() {
const res = await fetch(`${BASE_URL}/photos?user=${encodeURIComponent('Admin')}`, {
headers: { "Authorization": "Bearer " + token }
});
if (!res.ok) {
console.error("Errore readDB:", res.status);
return;
}
db = await res.json();
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
}
async function resetDB() {
await fetch(`${BASE_URL}/initDB`, {
headers: { "Authorization": "Bearer " + token }

View file

@ -6,10 +6,23 @@ async function apiGet(url) {
}).then(r => r.json());
}
async function getAllPhotos() {
return apiGet(`${BASE_URL}/photos`);
async function getAllPhotos1() {
//const payload = parseJwt(localStorage.getItem("token"));
//const user = payload.name || "Common";
//return apiGet(`${BASE_URL}/photos?user=${encodeURIComponent(user)}`);
return apiGet(`https://prova.patachina.it/photos?user=Fabio`);
}
async function getAllPhotos(user) {
return apiGet(`${BASE_URL}/photos?user=${encodeURIComponent(user)}`);
}
async function getPhotoById(id) {
const payload = parseJwt(localStorage.getItem("token"));
const user = payload.name || "Common";

View file

@ -175,13 +175,23 @@ async function initGallery() {
await incrementalSync();
console.log("[initGallery] incrementalSync() iniziale COMPLETATO");
console.log(`[initGallery] Avvio polling ogni ${refreshSeconds} secondi...`);
// ===============================================
// 🛟 POLLING DI SICUREZZA (configurabile da .env)
// ===============================================
if (refreshSeconds > 0) {
console.log(`[initGallery] Polling attivo ogni ${refreshSeconds} secondi...`);
setInterval(async () => {
console.log(">>> TIMER: chiamata incrementalSync()");
console.log(">>> lastSync attuale =", getLastSync());
await incrementalSync();
console.log(">>> incrementalSync() COMPLETATO (timer)");
}, refreshSeconds * 1000);
} else {
console.log("[initGallery] Polling disattivato (refreshSeconds = 0)");
}
}
window.addEventListener("DOMContentLoaded", initGallery);

View file

@ -171,48 +171,7 @@ document.getElementById('settingsBtn')?.addEventListener('click', () => {
window.location.href = "admin.html";
});
// ===============================
// LOGIN SUBMIT
// ===============================
/*
document.getElementById("loginSubmit").addEventListener("click", async () => {
const email = document.getElementById("loginEmail").value;
const password = document.getElementById("loginPassword").value;
const errorEl = document.getElementById("loginError");
errorEl.textContent = "";
try {
const res = await fetch(`${window.BASE_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password })
});
if (!res.ok) {
errorEl.textContent = "Utente o password errati";
return;
}
const data = await res.json();
const token = data.token;
localStorage.setItem("token", token);
window.token = token;
document.getElementById("loginModal").style.display = "none";
syncHeaderAuthUI();
// 🔥 NON caricare più foto da main.js
// initGallery() farà tutto
} catch (err) {
console.error("Errore login:", err);
errorEl.textContent = "Errore di connessione al server";
}
});
*/
// ===============================
// LOGIN SUBMIT

View file

@ -211,11 +211,14 @@ function startWebSocket() {
console.log("📩 [WS] Messaggio ricevuto:", msg);
if (msg.type === "photo_added") {
console.log(`🟢 [WS] Foto aggiunta → id=${msg.photo_id}`);
// FOTO AGGIUNTA (da /photos o auto_scan ADD)
if (msg.type === "added") {
console.log(`🟢 [WS] Foto aggiunta → id=${msg.photo_id || msg.id}`);
const id = msg.id;
try {
const arr = await getPhotoById(msg.photo_id);
const arr = await getPhotoById(id);
if (arr.length) {
console.log("📸 [WS] Foto trovata → aggiorno localPhotos");
@ -228,8 +231,40 @@ function startWebSocket() {
console.error("❌ [WS] Errore getPhotoById:", err);
}
}
// FOTO RIMOSSA (auto_scan DEL)
if (msg.type === "removed") {
const id = msg.id;
console.log(`🔴 [WS] Foto rimossa → id=${id}`);
removePhotoLocal(id);
const el = document.getElementById("photo_" + id);
if (el) {
console.log(`🗑️ [WS] Rimuovo elemento DOM: ${el.id}`);
el.remove();
}
refreshGallery();
}
// CARTELLA AGGIUNTA
if (msg.type === "add_dir") {
console.log(`📁 [WS] Cartella aggiunta → ${msg.folder}`);
// qui puoi decidere se rifare una sync parziale o solo aggiornare la lista cartelle
// per ora: incrementalSync "soft"
incrementalSync();
}
// CARTELLA RIMOSSA
if (msg.type === "del_dir") {
console.log(`📁 [WS] Cartella rimossa → ${msg.folder}`);
// idem come sopra
incrementalSync();
}
};
ws.onclose = () => {
console.warn("❌ [WS] Connessione chiusa → ritento tra 3s...");
setTimeout(startWebSocket, 3000);

View file

@ -1,253 +0,0 @@
// ===============================
// getPhotoById — versione corretta per incrementalSync
// ===============================
async function getPhotoById(id) {
const token = localStorage.getItem("token");
const payload = parseJwt(token);
const user = payload.name || "Common";
const url = `${BASE_URL}/photos/byIds?id=${encodeURIComponent(id)}&user=${encodeURIComponent(user)}`;
console.log("[DEBUG getPhotoById] URL:", url);
let res;
try {
res = await fetch(url, {
headers: { "Authorization": "Bearer " + token }
});
} catch (e) {
console.error("[getPhotoById] ERRORE FETCH:", e);
return [];
}
if (!res.ok) {
console.error("[getPhotoById] ERRORE HTTP:", res.status, res.statusText);
return [];
}
try {
const json = await res.json();
console.log("[DEBUG getPhotoById] JSON:", json);
return json;
} catch (e) {
console.error("[getPhotoById] ERRORE JSON:", e);
return [];
}
}
// ===============================
// sync.js — Sync incrementale con LOG DIAGNOSTICI COMPLETI
// ===============================
async function incrementalSync() {
console.log(">>> incrementalSync() START");
const payload = parseJwt(localStorage.getItem("token"));
const user = payload.name || "Common";
let lastSync = getLastSync();
const currentPhotos = getLocalPhotos();
console.log("[incrementalSync] lastSync =", lastSync, "user =", user);
console.log("[incrementalSync] localPhotos attuali =", currentPhotos.length);
// ============================================================
// 1) FULL LOAD OBBLIGATORIO
// ============================================================
if (!lastSync || currentPhotos.length === 0) {
console.log("[incrementalSync] Primo avvio → fullLoad() (forzato)");
try {
console.log("[DEBUG] Chiamo getAllPhotos()");
const photos = await getAllPhotos();
console.log(`[incrementalSync] fullLoad() ha caricato ${photos.length} foto`);
saveLocalState();
refreshGallery();
const now = new Date().toISOString();
setLastSync(now);
console.log("[incrementalSync] setLastSync (fullLoad) =", now);
console.log(">>> incrementalSync() COMPLETATO (fullLoad)");
return;
} catch (err) {
console.error("[incrementalSync] ERRORE in fullLoad:", err);
return;
}
}
// ============================================================
// 2) SYNC INCREMENTALE
// ============================================================
let changes;
try {
console.log(`[DEBUG] Chiamo getChanges(lastSync=${lastSync}, user=${user})`);
changes = await getChanges(lastSync, user);
} catch (err) {
console.error("[incrementalSync] ERRORE getChanges:", err);
return;
}
console.log("[incrementalSync] Cambiamenti ricevuti:", changes);
if (!changes || !changes.changes || changes.changes.length === 0) {
console.log("[incrementalSync] Nessun cambiamento → lastSync NON aggiornato");
return;
}
// ============================================================
// 3) APPLICA CAMBIAMENTI
// ============================================================
for (const ch of changes.changes) {
console.log("--------------------------------------------------");
console.log("[incrementalSync] Cambio:", ch);
// ---------------------------------------------------------
// FOTO AGGIUNTA
// ---------------------------------------------------------
if (ch.change_type === "added") {
console.log(`[DEBUG] Evento ADDED → richiedo getPhotoById(${ch.photo_id})`);
try {
const arr = await getPhotoById(ch.photo_id);
console.log(`[DEBUG getPhotoById] id=${ch.photo_id} → risposta:`, arr);
if (arr.length) {
console.log(`[DEBUG] Foto trovata → aggiungo a localPhotos id=${ch.photo_id}`);
addPhotoLocal(arr[0]);
} else {
console.warn(`[WARN] Foto NON trovata nel server remoto per id=${ch.photo_id}`);
}
} catch (err) {
console.error("[incrementalSync] ERRORE getPhotoById:", err);
}
}
// ---------------------------------------------------------
// FOTO RIMOSSA
// ---------------------------------------------------------
if (ch.change_type === "removed") {
console.log(`[DEBUG] Evento REMOVED → rimuovo foto id=${ch.photo_id}`);
removePhotoLocal(ch.photo_id);
const el = document.getElementById("photo_" + ch.photo_id);
if (el) {
console.log("[DEBUG] Rimuovo elemento DOM:", el.id);
el.remove();
}
}
}
console.log("[incrementalSync] localPhotos dopo sync:", getLocalPhotos().length);
// ============================================================
// 4) REFRESH UI
// ============================================================
console.log("[DEBUG] refreshGallery()");
refreshGallery();
// ============================================================
// 5) AGGIORNA lastSync
// ============================================================
const lastTimestamp = changes.changes.at(-1).timestamp || new Date().toISOString();
console.log(`[DEBUG] Aggiorno lastSync → ${lastTimestamp}`);
setLastSync(lastTimestamp);
console.log(">>> incrementalSync() COMPLETATO");
}
// ===============================
// JWT PARSER
// ===============================
function parseJwt(token) {
if (!token) return {};
try {
const base64 = token.split('.')[1];
const json = atob(base64);
return JSON.parse(json);
} catch (e) {
console.error("parseJwt error:", e);
return {};
}
}
// ===============================
// WEBSOCKET REAL-TIME
// ===============================
function startWebSocket() {
const token = localStorage.getItem("token");
if (!token) {
console.error("[WS] Nessun token JWT trovato");
return;
}
const payload = parseJwt(token);
const user = payload.name || "Common";
console.log("[WS] Connessione a wss://prova-ws.patachina.it ...");
const ws = new WebSocket("wss://prova-ws.patachina.it");
ws.onopen = () => {
console.log("[WS] Connesso, invio token JWT");
ws.send(JSON.stringify({
type: "auth",
token
}));
};
ws.onmessage = async (ev) => {
let msg;
try {
msg = JSON.parse(ev.data);
} catch (e) {
console.error("[WS] Errore parsing JSON:", e);
return;
}
console.log("[WS] Messaggio ricevuto:", msg);
// -------------------------------
// FOTO AGGIUNTA
// -------------------------------
if (msg.type === "photo_added") {
console.log(`[WS] Foto aggiunta → id=${msg.photo_id}`);
try {
const arr = await getPhotoById(msg.photo_id);
if (arr.length) {
console.log("[WS] Foto trovata, aggiungo a localPhotos");
addPhotoLocal(arr[0]);
refreshGallery();
} else {
console.warn("[WS] Foto non trovata nel server remoto");
}
} catch (err) {
console.error("[WS] Errore getPhotoById:", err);
}
}
};
ws.onclose = () => {
console.warn("[WS] Connessione chiusa, ritento tra 3s...");
setTimeout(startWebSocket, 3000);
};
ws.onerror = (err) => {
console.error("[WS] Errore WebSocket:", err);
};
}
// Avvio automatico del WebSocket
startWebSocket();
window.incrementalSync = incrementalSync;
window.parseJwt = parseJwt;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 MiB

View file

@ -1 +0,0 @@
prova

View file

@ -1 +1 @@
{"current":103,"total":103,"percent":100,"eta":"00:00:00","elapsed":"00:00:01"}
{"current":22,"total":22,"percent":100,"eta":"00:00:00","elapsed":"00:00:11"}

File diff suppressed because it is too large Load diff

View file

@ -3,8 +3,6 @@ const express = require('express');
const router = express.Router();
const db = require('../db/knex');
// 🔥 IMPORTANTE: aggiungi questa riga
const wss = require('../ws-server');
// -----------------------------------------------------
@ -26,7 +24,6 @@ function normalizePhoto(p) {
_indexHash: p._indexHash || null,
// Rimuovo campi DB non necessari
lat: undefined,
lon: undefined,
alt: undefined,
@ -44,7 +41,6 @@ router.post('/', async (req, res) => {
try {
const p = req.body;
// 1) Inserisci la foto nel DB remoto
console.log(`📸 [DB INSERT] Salvo foto id=${p.id} user=${p.user}`);
await db('photos').insert({
id: p.id,
@ -71,7 +67,6 @@ router.post('/', async (req, res) => {
fast_hash: p.fast_hash
});
// 2) Registra levento ADDED
console.log(`🟢 [DB CHANGE] Inserisco evento ADDED per id=${p.id}`);
await db('photo_changes').insert({
photo_id: p.id,
@ -80,10 +75,9 @@ router.post('/', async (req, res) => {
timestamp: new Date().toISOString()
});
// 🔥 3) INVIO EVENTO WEBSOCKET IN TEMPO REALE
console.log(`📡 [WS] Invio evento real-time a user=${p.user}`);
wss.broadcastToUser(p.user, {
type: "photo_added",
type: "added",
photo_id: p.id
});
@ -97,19 +91,29 @@ router.post('/', async (req, res) => {
});
// -----------------------------------------------------
// GET /photos → restituisce TUTTE le foto dellutente + Common
// GET /photos → Admin vede tutto, altri vedono solo loro + Common
// -----------------------------------------------------
router.get('/', async (req, res) => {
console.log("📤 [GET /photos] Richiesta ricevuta");
const user = req.query.user || req.user?.name;
console.log("📤 [GET /photos] Richiesta ricevuta user=", user);
try {
const rows = await db('photos')
let rows;
if (user === "Admin") {
console.log("👑 Admin → restituisco TUTTE le foto");
rows = await db('photos')
.select('*')
.orderBy('mtimeMs', 'desc');
} else {
console.log(`👤 Utente normale → foto di ${user} + Common`);
rows = await db('photos')
.whereIn('user', [user, "Common"])
.orderBy('mtimeMs', 'desc');
}
console.log(`📤 [GET /photos] Restituisco ${rows.length} foto`);
const normalized = rows.map(normalizePhoto);
res.json(normalized);
res.json(rows.map(normalizePhoto));
} catch (err) {
console.error("❌ Errore GET /photos:", err);
@ -118,7 +122,7 @@ router.get('/', async (req, res) => {
});
// -----------------------------------------------------
// GET /photos/changes
// GET /photos/changes → Admin vede tutto
// -----------------------------------------------------
router.get('/changes', async (req, res) => {
const since = req.query.since;
@ -127,15 +131,24 @@ router.get('/changes', async (req, res) => {
console.log(`📤 [GET /photos/changes] since=${since} users=${users}`);
if (!since || !users.length) {
console.log("❌ Parametri mancanti");
return res.status(400).json({ error: "Parametri richiesti: since, user" });
}
try {
const rows = await db('photo_changes')
let rows;
if (users.includes("Admin")) {
console.log("👑 Admin → restituisco TUTTI i cambiamenti");
rows = await db('photo_changes')
.where('timestamp', '>', since)
.orderBy('timestamp', 'asc');
} else {
console.log("👤 Utente normale → cambiamenti filtrati");
rows = await db('photo_changes')
.where('timestamp', '>', since)
.whereIn('user', users)
.orderBy('timestamp', 'asc');
}
console.log(`📤 [GET /photos/changes] Restituisco ${rows.length} cambiamenti`);
res.json({ changes: rows });
@ -147,41 +160,35 @@ router.get('/changes', async (req, res) => {
});
// -----------------------------------------------------
// GET /photos/byIds
// GET /photos/byIds → Admin vede tutto
// -----------------------------------------------------
router.get('/byIds', async (req, res) => {
const ids = req.query.id;
const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user];
console.log("📤 [GET /photos/byIds] ids:", ids);
console.log("📤 [GET /photos/byIds] users (raw):", req.query.user);
console.log("📤 [GET /photos/byIds] users (array):", users);
console.log("📤 [GET /photos/byIds] users:", users);
if (!ids) {
console.log("⚠️ Nessun id richiesto → []");
return res.json([]);
}
if (!users || users.length === 0 || users[0] === undefined) {
console.log("❌ [GET /photos/byIds] ERRORE: 'user' NON passato dal frontend!");
}
if (!ids) return res.json([]);
const list = Array.isArray(ids) ? ids : [ids];
console.log("📤 [GET /photos/byIds] lista finale id:", list);
try {
console.log("📤 [GET /photos/byIds] Eseguo query con:");
console.log(" → id IN", list);
console.log(" → user IN", users);
let rows;
const rows = await db('photos')
if (users.includes("Admin")) {
console.log("👑 Admin → byIds senza filtro user");
rows = await db('photos')
.whereIn('id', list);
} else {
console.log("👤 Utente normale → byIds filtrato");
rows = await db('photos')
.whereIn('id', list)
.whereIn('user', users);
}
console.log(`📤 [GET /photos/byIds] Query completata → trovate ${rows.length} foto`);
const normalized = rows.map(normalizePhoto);
res.json(normalized);
console.log(`📤 [GET /photos/byIds] trovate ${rows.length} foto`);
res.json(rows.map(normalizePhoto));
} catch (err) {
console.error("❌ Errore /photos/byIds:", err);

193
routes/photos.js.old Normal file
View file

@ -0,0 +1,193 @@
// routes/photos.js
const express = require('express');
const router = express.Router();
const db = require('../db/knex');
// 🔥 IMPORTANTE: aggiungi questa riga
const wss = require('../ws-server');
// -----------------------------------------------------
// Normalizzazione record DB → formato identico al vecchio index.json
// -----------------------------------------------------
function normalizePhoto(p) {
return {
...p,
gps: (p.lat != null && p.lon != null)
? {
lat: p.lat,
lng: p.lon,
alt: p.alt ?? null
}
: null,
location: p.location ? JSON.parse(p.location) : null,
_indexHash: p._indexHash || null,
// Rimuovo campi DB non necessari
lat: undefined,
lon: undefined,
alt: undefined,
location_json: undefined
};
}
// -----------------------------------------------------
// POST /photos → salva una foto inviata dallo scanner locale
// -----------------------------------------------------
router.post('/', async (req, res) => {
console.log("📥 [POST /photos] Richiesta ricevuta");
console.log("📥 Body:", req.body);
try {
const p = req.body;
// 1) Inserisci la foto nel DB remoto
console.log(`📸 [DB INSERT] Salvo foto id=${p.id} user=${p.user}`);
await db('photos').insert({
id: p.id,
user: p.user,
cartella: p.cartella,
name: p.name,
path: p.path,
thub1: p.thub1,
thub2: p.thub2,
mime_type: p.mime_type,
width: p.width,
height: p.height,
rotation: p.rotation,
size_bytes: p.size_bytes,
mtimeMs: p.mtimeMs,
duration_ms: p.duration_ms,
taken_at: p.taken_at,
data: p.data,
lat: p.gps?.lat ?? null,
lon: p.gps?.lng ?? null,
alt: p.gps?.alt ?? null,
location: p.location ? JSON.stringify(p.location) : null,
_indexHash: p._indexHash,
fast_hash: p.fast_hash
});
// 2) Registra levento ADDED
console.log(`🟢 [DB CHANGE] Inserisco evento ADDED per id=${p.id}`);
await db('photo_changes').insert({
photo_id: p.id,
user: p.user,
change_type: 'added',
timestamp: new Date().toISOString()
});
// 🔥 3) INVIO EVENTO WEBSOCKET IN TEMPO REALE
console.log(`📡 [WS] Invio evento real-time a user=${p.user}`);
wss.broadcastToUser(p.user, {
type: "added",
photo_id: p.id
});
console.log(`✅ [POST /photos] Foto id=${p.id} salvata con successo`);
res.json({ ok: true });
} catch (err) {
console.error("❌ Errore POST /photos:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// GET /photos → restituisce TUTTE le foto dellutente + Common
// -----------------------------------------------------
router.get('/', async (req, res) => {
const user = req.query.user || req.user?.name;
console.log("📤 [GET /photos] Richiesta ricevuta");
try {
const rows = await db('photos')
.whereIn('user', [user, "Common"])
.orderBy('mtimeMs', 'desc');
console.log(`📤 [GET /photos] user=${user} Restituisco ${rows.length} foto`);
const normalized = rows.map(normalizePhoto);
res.json(normalized);
} catch (err) {
console.error("❌ Errore GET /photos:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// GET /photos/changes
// -----------------------------------------------------
router.get('/changes', async (req, res) => {
const since = req.query.since;
const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user];
console.log(`📤 [GET /photos/changes] since=${since} users=${users}`);
if (!since || !users.length) {
console.log("❌ Parametri mancanti");
return res.status(400).json({ error: "Parametri richiesti: since, user" });
}
try {
const rows = await db('photo_changes')
.where('timestamp', '>', since)
.whereIn('user', users)
.orderBy('timestamp', 'asc');
console.log(`📤 [GET /photos/changes] Restituisco ${rows.length} cambiamenti`);
res.json({ changes: rows });
} catch (err) {
console.error("❌ Errore /photos/changes:", err);
res.status(500).json({ error: err.message });
}
});
// -----------------------------------------------------
// GET /photos/byIds
// -----------------------------------------------------
router.get('/byIds', async (req, res) => {
const ids = req.query.id;
const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user];
console.log("📤 [GET /photos/byIds] ids:", ids);
console.log("📤 [GET /photos/byIds] users (raw):", req.query.user);
console.log("📤 [GET /photos/byIds] users (array):", users);
if (!ids) {
console.log("⚠️ Nessun id richiesto → []");
return res.json([]);
}
if (!users || users.length === 0 || users[0] === undefined) {
console.log("❌ [GET /photos/byIds] ERRORE: 'user' NON passato dal frontend!");
}
const list = Array.isArray(ids) ? ids : [ids];
console.log("📤 [GET /photos/byIds] lista finale id:", list);
try {
console.log("📤 [GET /photos/byIds] Eseguo query con:");
console.log(" → id IN", list);
console.log(" → user IN", users);
const rows = await db('photos')
.whereIn('id', list)
.whereIn('user', users);
console.log(`📤 [GET /photos/byIds] Query completata → trovate ${rows.length} foto`);
const normalized = rows.map(normalizePhoto);
res.json(normalized);
} catch (err) {
console.error("❌ Errore /photos/byIds:", err);
res.status(500).json({ error: err.message });
}
});
module.exports = router;

554
scanPhoto_how_works.md Normal file
View file

@ -0,0 +1,554 @@
📌 1. Avvio dello scan
- Parte con un log: "Inizio scan globale per user=X".
- Calcola il percorso della cartella photos/<user>/original.
- Conta velocemente quanti file supportati esistono (con countFilesFast).
- Se non ci sono file → termina.
📊 2. Gestione dello stato di avanzamento
Durante lo scan:
- Tiene traccia di:
- CURRENT = quanti file ha già processato
- TOTAL_FILES = quanti file totali ci sono
- Aggiorna un file JSON pubblico:
public/photos/scan_status.json
Questo file contiene:
- current
- total
- percent
- elapsed
- eta
Serve per mostrare una barra di avanzamento in tempo reale.
📁 3. Scansione delle cartelle dellutente
Per ogni sottocartella dentro original/:
- Chiama scanSingleFolder(user, cartella, absCartella).
Dentro questa funzione succede il grosso del lavoro.
🔍 4. Per ogni file trovato:
scanCartella() restituisce oggetti con:
- id (deterministico)
- name
- relPath
- absPath
- ext
- stat (size, mtime, ecc.)
- path normalizzato
user=Fabio cartella=2017Irlanda19-29ago relPath=VID_20260317_115145.mp4 id=51a7e1861de8da5147bbbc35d70a0128fcd2a0451c7121a8bf2412cb2903d469
Per ogni file:
🟦 4.1 Recupera eventuale record esistente dal DB
`js
prev = await db("photos").where({ id }).first();
`
🟩 4.2 Se il path è cambiato → considerato nuovo
`js
if (prev && prev.path !== f.path) prev = null;
`
🟢 4.3 Se è un nuovo file
- Log dettagliato
- Chiama processFile() per generare metadati, thumbnails, EXIF, ecc.
- Inserisce nel DB
- Registra un evento in photo_changes
- Aggiunge a newFiles
🔵 4.4 Fast-skip (file invariato)
Due livelli:
1. FAST-SIZE-SKIP
Se la size è identica → aggiorna solo path/mtime/hash.
2. FAST-HASH-SKIP
Se lhash veloce (sha256(size-mtime)) è identico → aggiorna solo path/mtime.
🟠 4.5 File modificato
Se non passa i fast-skip:
- Rielabora il file con processFile()
- Aggiorna il DB con .insert().onConflict().merge()
- Registra evento updated
- Aggiunge a newFiles
---
🗑️ 5. Gestione degli orfani
Alla fine della cartella:
- Tutti gli ID presenti nel DB ma non trovati sul disco vengono:
- rimossi dal DB
- rimossi i thumbnails
- loggati come cancellati
---
🌐 6. Invio al server remoto (opzionale)
Se SENDPHOTOS=true e BASEURL è configurato:
- Ogni nuovo file viene inviato via postWithAuth() al server remoto.
Se non configurato:
- Logga [NO SEND].
---
🟣 7. Fine scan
Logga il tempo totale e restituisce larray newFiles.
---
🧩 In sintesi
scanPhoto.js è un motore di sincronizzazione locale → database → server remoto.
Fa tutto questo:
| Funzione | Descrizione |
|---------|-------------|
| Scansione ricorsiva | Trova tutti i file supportati nelle cartelle dellutente |
| Confronto con DB | Determina se un file è nuovo, modificato o invariato |
| Fast-skip | Evita elaborazioni inutili usando size e hash veloce |
| Elaborazione file | Chiama processFile() per EXIF, thumbnails, metadati |
| Aggiornamento DB | Inserisce/aggiorna record in photos |
| Rilevamento orfani | Cancella dal DB file non più presenti sul disco |
| Log avanzamento | Scrive un file JSON con percentuale, ETA, ecc. |
| Invio remoto | Invia i nuovi file a un server remoto (se abilitato) |
_____
functio countFilesFast(rootDir)
conta i file nelle cartelle photos/<user>/original
function formatTime(ms)
trasforma ms in h:m:s
---
async function scanPhoto(dir, userName, db)
const photosRoot
è il path assoluto <app>/public/photos
const userDir
è il path assoluto <app>/public/photos/<user>/original
function updateStatusFile()
fa l'aggiornamento dello stato di scansione dei file
function scanSingleFolder(user, cartella, absCartella)
user: Fabio
cartella: 2017Irlanda19-29ago
absCartella: /home/nvme/dockerdata/prove/s15j/public/photos/Fabio/original/2017Irlanda19-29ago
_____
🧩 Cosa fa esattamente questa riga
```js
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
```
---
Quella riga fa una cosa molto semplice… ma potentissima.
E soprattutto è il cuore della pulizia degli orfani.
📌 0. definita da
```
const createCleanupFunctions = require('./orphanCleanup');
```
🔍 1. Chiama createCleanupFunctions(db)
createCleanupFunctions è una funzione che riceve il database (db) e restituisce un oggetto contenente due funzioni:
- deleteThumbsById(id)
- deleteFromDB(id, user)
In pratica, è un factory: crea funzioni già configurate con il DB.
---
📌 2. Cosa fanno le due funzioni restituite
---
🗑️ deleteThumbsById(id)
Cancella i thumbnails associati a una foto:
- thub1
- thub2
- eventuali altre miniature generate
Serve quando un file non esiste più sul disco → quindi i suoi thumbnails diventano spazzatura.
---
🗄️ deleteFromDB(id, user)
Cancella dal database:
- la riga nella tabella photos
- eventuali metadati collegati
- eventuali record in photo_changes (dipende da come lhai implementato)
È la parte che rimuove lentry dal DB quando il file è sparito dal filesystem.
---
🔴 3. Perché serve?
Durante lo scan, alla fine di ogni cartella, hai:
`js
for (const orphanId of idsSet) {
log( 🔴 Cancellato ${orphanId});
await deleteThumbsById(orphanId);
await deleteFromDB(orphanId, user);
}
`
idsSet contiene gli ID presenti nel DB ma non trovati sul disco.
Quindi:
- deleteThumbsById() → pulisce i file fisici
- deleteFromDB() → pulisce il database
È la logica che mantiene il sistema coerente.
---
🧠 In sintesi
Quella riga:
`js
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
`Eccoti una spiegazione cristallina e completa di cosa fa:
`js
async function scanSingleFolder(user, cartella, absCartella)
`
È una funzione fondamentale: gestisce tutta la sincronizzazione tra filesystem e database per una singola cartella dellutente.
---
📁 Obiettivo della funzione
Scansionare una cartella (es. DCIM, Vacanze2023, WhatsApp Images) e:
1. Trovare i file presenti sul disco
2. Confrontarli con il database
3. Capire cosa è nuovo, cosa è modificato, cosa è invariato
4. Aggiornare il DB
5. Elaborare i nuovi file (EXIF, thumbnails, metadati)
6. Rimuovere dal DB i file orfani (presenti nel DB ma non più sul disco)
È il cuore della logica di sincronizzazione.
---
🧠 Funzionamento passo per passo
1⃣ Log di inizio
`js
log(📁 Inizio cartella: ${cartella});
`
---
2⃣ Recupera dal DB tutti gli ID già conosciuti
`js
const rows = await db('photos')
.where({ user, cartella })
.select('id');
const idsSet = new Set(rows.map(r => r.id));
`
idsSet serve per capire quali file non verranno trovati sul disco → saranno orfani.
---
3⃣ Itera su tutti i file trovati sul disco
`js
for await (const f of scanCartella(...)) {
`
scanCartella() restituisce per ogni file:
- id deterministico
- name
- path normalizzato
- relPath, absPath
- ext
- stat (size, mtime, ecc.)
---
4⃣ Aggiorna lo stato di avanzamento
Incrementa CURRENT e aggiorna il file JSON con percentuale, ETA, ecc.
---
5⃣ Recupera eventuale record esistente dal DB
`js
let prev = await db("photos")
.select(...)
.where({ id })
.first();
`
---
6⃣ Calcola il fast-hash
`js
const fastHash = sha256(${st.size}-${st.mtimeMs});
`
Serve per capire velocemente se il file è cambiato.
---
🔍 7⃣ Logica decisionale per ogni file
🟢 Caso A — File nuovo
Se prev è null:
- Log
- Chiama processFile() per estrarre metadati, EXIF, thumbnails
- Inserisce nel DB
- Registra evento added
- Aggiunge a newFiles
- Rimuove lID da idsSet
---
🔵 Caso B — File invariato (fast-skip)
1. FAST-SIZE-SKIP
Se la size è identica → file invariato.
2. FAST-HASH-SKIP
Se lhash veloce è identico → invariato.
In entrambi i casi:
- Aggiorna solo path/mtime/hash nel DB
- Rimuove lID da idsSet
---
🟠 Caso C — File modificato
Se non passa i fast-skip:
- Log
- Chiama processFile()
- Aggiorna il DB con .insert().onConflict().merge()
- Registra evento updated
- Aggiunge a newFiles
- Rimuove lID da idsSet
---
🔴 8⃣ Gestione degli orfani
Alla fine della cartella:
`js
for (const orphanId of idsSet) {
await deleteThumbsById(orphanId);
await deleteFromDB(orphanId, user);
}
`
Questi sono file che:
- esistono nel DB
- non sono stati trovati sul disco
Quindi vengono eliminati.
---
🧩 In sintesi
scanSingleFolder():
| Step | Cosa fa |
|------|---------|
| 1 | Carica dal DB gli ID della cartella |
| 2 | Scansiona i file reali sul disco |
| 3 | Per ogni file decide: nuovo, modificato, invariato |
| 4 | Elabora i nuovi/modificati con processFile() |
| 5 | Aggiorna o inserisce nel DB |
| 6 | Registra gli eventi in photo_changes |
| 7 | Rimuove gli orfani (DB → disco) |
| 8 | Restituisce i nuovi file |
È la funzione che mantiene coerenza totale tra filesystem e database.
---
Se vuoi, posso anche:
- disegnarti un diagramma di flusso
- riscrivere la funzione in modo più modulare
- aggiungere logging più leggibile
- ottimizzare la parte di fast-skip
- aggiungere batching per il DB
Dimmi tu come vuoi procedere.
fa questo:
- chiama una funzione factory
- che crea due funzioni già configurate con il DB
- che servono per cancellare foto orfane (thumbnails + DB)
È un modo elegante per incapsulare la logica di cleanup.
___
Eccoti la spiegazione chiara, completa e tecnica di cosa fa scanCartella.js e cosa restituisce.
---
📌 Cosè scanCartella?
È una funzione asincrona generator (async function*) che:
- scansiona ricorsivamente una cartella sul filesystem
- filtra solo i file con estensioni supportate
- calcola un ID deterministico per ogni file
- raccoglie informazioni minime (stat, path, nome, estensione…)
- NON elabora il file (niente EXIF, niente thumbnails)
- NON scrive sul database
👉 Il suo unico scopo è enumerare i file reali presenti sul disco e restituire un oggetto per ciascuno.
È volutamente “leggera”: tutta la logica pesante (processFile, DB, confronto) è in scanPhoto.
---
🧠 Cosa fa, passo per passo
1⃣ Legge la cartella ricorsivamente
La funzione interna walk():
- usa fsp.readdir() con { withFileTypes: true }
- entra nelle sottocartelle
- ignora file non supportati
---
2⃣ Filtra per estensioni supportate
`js
if (!SUPPORTED_EXTS.has(ext)) continue;
`
---
3⃣ Costruisce il percorso relativo
`js
const fileRelPath = relPath ? ${relPath}/${e.name} : e.name;
`
---
4⃣ Genera un ID deterministico
`js
const id = sha256(${userName}/${cartella}/${fileRelPath});
`
Questo ID è:
- stabile
- identico tra scansioni
- basato su user/cartella/percorso relativo
È fondamentale per confrontare file reali ↔ DB.
---
5⃣ Legge lo stat del file
`js
st = await fsp.stat(absPath);
`
Serve per:
- size
- mtimeMs
- fast-hash
- confronto rapido in scanPhoto
---
6⃣ Logga il file trovato
`js
log(📂 scanCartella → user=${userName} cartella=${cartella} relPath=${fileRelPath} id=${id});
`
---
7⃣ Restituisce un oggetto per ogni file
Usa yield per restituire uno stream di risultati, non un array.
---
📤 Cosa restituisce esattamente?
Per ogni file valido, restituisce un oggetto:
`js
{
id, // ID deterministico
user: userName, // nome utente
cartella, // nome cartella (es. DCIM)
name: e.name, // nome file
relPath: fileRelPath, // percorso relativo dentro la cartella
absPath, // percorso assoluto sul disco
ext, // estensione (es. .jpg)
stat: st, // fs.stat() → size, mtimeMs, ecc.
path: /photos/${userName}/original/${cartella}/${fileRelPath}
}
`
---
🧩 In sintesi
✔️ Cosa fa
- Scansiona ricorsivamente una cartella
- Filtra solo i file supportati
- Genera ID deterministici
- Legge stat del file
- Costruisce percorsi relativi/assoluti
- Restituisce un oggetto per ogni file
❌ Cosa NON fa
- Non elabora il file (niente EXIF, niente thumbnails)
- Non scrive nel DB
- Non decide se il file è nuovo/modificato
- Non cancella orfani
---
🎯 A cosa serve?
È il motore di enumerazione dei file.
scanPhoto() usa i dati prodotti da scanCartella() per:
- confrontare con il DB
- decidere se un file è nuovo/modificato/invariato
- elaborarlo con processFile()
- aggiornare il DB
- eliminare orfani
---
Se vuoi, posso anche:
- disegnarti un diagramma di flusso
- riscrivere scanCartella in modo più modulare
- aggiungere caching o parallelizzazione
- aggiungere controlli per file corrotti
Dimmi tu come vuoi procedere.

471
scan_Funzionamento.md Normal file
View file

@ -0,0 +1,471 @@
Perfetto Fabio, ora che il server è sistemato posso spiegarti **esattamente come funziona lo scan adesso**, in modo chiaro, ordinato e soprattutto *vero* rispetto al codice che hai.
Ti faccio una spiegazione **operativa**, come se stessi guardando il flusso in tempo reale.
---
# 🔵 **1. Lutente chiama `/scan`**
Quando fai:
```
GET /scan
```
succede questo:
### ✔ Il middleware JWT autentica lutente
### ✔ Il middleware GET *non aggiunge più “Common”*
→ quindi lo scan viene fatto **solo per lutente reale**
→ niente doppie scansioni
→ niente doppio removed
---
# 🔵 **2. Il server chiama `scanPhotosUser(userName, db)`**
Esempio:
```
scanPhotosUser("Fabio", db)
```
Questa funzione fa due passaggi:
---
# 🟣 **PASSAGGIO 1 — Conta i file reali (ricorsivo)**
`countFilesUser()`:
- entra in `/photos/Fabio/original`
- scende in tutte le sottocartelle
- conta solo i file con estensioni supportate
- NON tocca il DB
- NON scansiona due volte
- NON elimina orfani
Questo serve solo per:
- mostrare il progresso
- calcolare la percentuale
- stimare il tempo
E ora funziona correttamente.
---
# 🟣 **PASSAGGIO 2 — Scansiona ogni cartella UNA sola volta**
Per ogni cartella dentro:
```
/photos/Fabio/original/
```
viene chiamato:
```
scanPhoto(cartella, userName, db, ...)
```
---
# 🔵 **3. `scanPhoto()` chiama `scanPhotoCartella()` UNA sola volta**
E qui avviene la vera magia.
---
# 🟣 **Dentro `scanPhotoCartella()` succede questo:**
### 1⃣ Recupera dal DB tutti gli ID della cartella
→ costruisce `idsSet`
### 2⃣ Scansiona i file reali con `scanCartella()`
→ per ogni file trovato:
- aggiorna il progresso
- chiama `scanPhotoSingle()` per processarlo
- elimina lID da `idsSet` (non è orfano)
### 3⃣ Alla fine, gli ID rimasti in `idsSet` sono **orfani veri**
### 4⃣ Per ogni orfano:
- elimina thumbs
- elimina dal DB
- registra **1 solo removed**
---
# 🔥 **Perché ora NON hai più 2 removed?**
Perché:
- `/scan` viene chiamato **una sola volta**
- `scanPhotosUser` viene eseguito **una sola volta**
- `scanPhoto` viene eseguito **una sola volta**
- `scanPhotoCartella` viene eseguito **una sola volta**
- gli orfani vengono eliminati **una sola volta**
Prima invece:
- il middleware GET aggiungeva **Common**
- quindi `/scan` scansionava **Fabio + Common**
- Common vedeva gli ID di Fabio come orfani
→ removed
- Fabio vedeva gli ID come orfani
→ removed
- totale: **2 removed**
Ora questo non succede più.
---
# 🟢 **RISULTATO FINALE DEL NUOVO SISTEMA**
### ✔ 1 added quando aggiungi una foto
### ✔ 1 removed quando la togli
### ✔ nessun doppio evento
### ✔ nessuna doppia scansione
### ✔ conteggio file corretto
### ✔ comportamento stabile e identico al vecchio sistema
---
Perfetto Fabio — ti preparo **un diagramma chiaro, leggibile e fedele al comportamento reale del tuo sistema**, così puoi vedere tutto il flusso in un colpo docchio.
Lo faccio in stile **flowchart testuale**, così puoi anche stamparlo o incollarlo nei tuoi appunti.
---
# 🔵 **DIAGRAMMA COMPLETO DELLO SCAN (versione attuale, corretta)**
```
┌──────────────────────────────┐
│ CLIENT /scan │
└───────────────┬──────────────┘
┌──────────────────────────────────────────────┐
│ 1. Middleware JWT │
│ ✔ Autentica lutente │
│ ✔ Imposta req.user │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 2. Middleware GET (PATCH) │
│ ✔ NON aggiunge più "Common" │
│ ✔ Per /scan → req.query.user = [utente] │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 3. Route /scan │
│ chiama: scanPhotosUser(req.user.name, db) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 4. scanPhotosUser(user) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 4.1 PRIMA PASSATA (ricorsiva) │ │
│ │ countFilesUser() │ │
│ │ ✔ entra in tutte le sottocartelle│ │
│ │ ✔ conta SOLO i file supportati │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 4.2 SECONDA PASSATA │ │
│ │ per ogni cartella in /original │ │
│ │ → scanPhoto(cartella) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 5. scanPhoto(cartella) │
│ chiama: scanPhotoCartella() │
│ (UNA sola volta per cartella) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 6. scanPhotoCartella() │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 6.1 Recupera ID dal DB │ │
│ │ → idsSet = { id1, id2, ... } │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 6.2 Scansione reale dei file │ │
│ │ for await (f of scanCartella) │ │
│ │ → scanPhotoSingle(f) │ │
│ │ → idsSet.delete(f.id) │ │
│ └──────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ 6.3 ORFANI (alla fine) │ │
│ │ per ogni id rimasto in idsSet: │ │
│ │ → deleteThumbsById │ │
│ │ → deleteFromDB │ │
│ │ → recordChange("removed") │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 7. scanPhoto ritorna newFiles │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 8. scanPhotosUser aggrega tutti i newFiles │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 9. /scan risponde al client │
└──────────────────────────────────────────────┘
```
---
# 🟢 **Cosa garantisce questo flusso**
### ✔ `/scan` viene eseguito UNA sola volta
### ✔ `scanPhotosUser` viene eseguito UNA sola volta
### ✔ `scanPhoto` viene eseguito UNA sola volta per cartella
### ✔ `scanPhotoCartella` viene eseguito UNA sola volta
### ✔ gli orfani vengono eliminati UNA sola volta
### ✔ 1 added, 1 removed (mai più 2 removed)
### ✔ conteggio file corretto
### ✔ nessuna interferenza con “Common”
---
# 🔥 Vuoi anche la versione “diagramma a blocchi” o “diagramma a frecce” più compatto?
# Se vuoi, posso anche farti un diagramma visivo del flusso completo, così lo vedi in un colpo docchio.
┌──────────────┐
│ /scan │
└───────┬──────┘
┌──────────────────────────┐
│ 1. Autenticazione JWT │
└──────────────────────────┘
┌────────────────────────────────────────┐
│ 2. Middleware GET (PATCH applicata) │
│ → NON aggiunge più "Common" │
│ → /scan usa SOLO req.user.name │
└────────────────────────────────────────┘
┌────────────────────────────────┐
│ 3. scanPhotosUser(user) │
└────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 4. PRIMA PASSATA (ricorsiva) │
│ countFilesUser() │
│ → entra in tutte le sottocartelle │
│ → conta SOLO i file supportati │
│ → NON tocca DB │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 5. SECONDA PASSATA │
│ per ogni cartella in /original: │
│ → scanPhoto(cartella) │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 6. scanPhoto(cartella) │
│ → chiama scanPhotoCartella() UNA volta │
└──────────────────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 7. scanPhotoCartella() │
│ │
│ 7.1 Recupera ID dal DB → idsSet │
│ 7.2 Scansiona file reali (scanCartella) │
│ → scanPhotoSingle() │
│ → idsSet.delete(id) │
│ 7.3 ORFANI (solo alla fine) │
│ → deleteThumbsById │
│ → deleteFromDB │
│ → recordChange("removed") │
└──────────────────────────────────────────────┘
┌────────────────────────────────┐
│ 8. scanPhoto ritorna newFiles │
└────────────────────────────────┘
┌──────────────────────────────────────────────┐
│ 9. scanPhotosUser aggrega tutti i newFiles │
└──────────────────────────────────────────────┘
┌──────────────────────────┐
│ 10. Risposta al client │
└──────────────────────────┘
Perfetto Fabio — ora che abbiamo integrato **la cancellazione delle cartelle eliminate** e la funzione modulare `deleteFolderForUser()`, ti spiego **esattamente cosa fa /scan adesso**, passo per passo, in modo chiaro e completo.
Questa è la versione *vera*, aggiornata, precisa del comportamento attuale.
---
# 🔵 **1. Lutente chiama `/scan`**
### ✔ Se è **Admin**
- scansiona **Common**
- scansiona **tutti gli utenti** presenti in `users.json`
### ✔ Se è **utente normale**
- scansiona **solo il proprio spazio**
---
# 🔵 **2. Dentro `scanPhotosUser(user)` succede questo**
## **Fase A — Lettura cartelle reali**
Legge:
```
/photos/<user>/original/
```
e ottiene la lista delle cartelle *realmente presenti* sul filesystem.
Esempio:
```
["Vacanze2023", "Lavoro", "Gatti"]
```
---
# 🔵 **3. Rilevamento cartelle eliminate (NOVITÀ)**
Ora cè la parte nuova e fondamentale:
### ✔ Recupera tutte le cartelle presenti nel DB per quellutente
(es. cartelle che contengono record)
### ✔ Confronta DB vs filesystem
Se nel DB cè:
```
["Vacanze2023", "Lavoro", "Gatti", "VecchiaCartella"]
```
ma sul disco cè:
```
["Vacanze2023", "Lavoro", "Gatti"]
```
allora:
```
"VecchiaCartella" è stata cancellata dal filesystem
```
### 👉 A questo punto viene chiamata:
```
deleteFolderForUser(user, "VecchiaCartella")
```
Che fa:
- elimina **tutti i record DB** della cartella
- registra **removed** per ogni file
- elimina **tutta la cartella thumbs residua**:
```
/photos/<user>/thumbs/VecchiaCartella/
```
### 🔥 Risultato:
**DB pulito + thumbs puliti**
anche se la cartella originale è stata cancellata manualmente.
---
# 🔵 **4. Fase B — Conteggio file reali**
Viene eseguita la prima passata ricorsiva:
```
countFilesUser()
```
Serve solo per:
- contare i file reali
- calcolare la percentuale
- mostrare il progresso
Non modifica nulla.
---
# 🔵 **5. Fase C — Scansione vera (UNA sola volta per cartella)**
Per ogni cartella reale:
```
scanPhoto(cartella)
```
Che:
- scansiona i file reali
- aggiorna il DB
- genera “added”
- rileva orfani **solo dentro quella cartella**
- elimina thumbs e DB per gli orfani
- registra “removed”
---
# 🔵 **6. Fine scan**
Restituisce:
- lista dei nuovi file
- log completo
- DB aggiornato
- thumbs aggiornati
---
# 🟢 **RIASSUNTO: COSA FA /scan ADESSO**
### ✔ Scansiona solo cartelle realmente presenti
### ✔ Rileva cartelle eliminate dal filesystem
### ✔ Cancella TUTTI i thumbs residui della cartella eliminata
### ✔ Cancella TUTTI i record DB della cartella eliminata
### ✔ Registra “removed” per ogni file eliminato
### ✔ Scansiona ogni cartella UNA sola volta
### ✔ Nessun doppio removed
### ✔ Nessun orfano nascosto
### ✔ Nessuna doppia passata
### ✔ Nessun conflitto tra utenti
### ✔ Admin può scansionare tutti
---
# 🔥 Vuoi che ti preparo anche un diagramma aggiornato del flusso `/scan` con la nuova logica di cancellazione cartelle?

173
server.js
View file

@ -8,12 +8,30 @@ const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const scanPhoto = require('./api_v1/scanner/scanPhoto.js');
const { handleAddFile, handleDeleteFile, handleAddDir, handleDeleteDir } = require('./api_v1/scanner/scanAuto.js');
const { WEB_ROOT } = require('./api_v1/config');
const config = require('./api_v1/config');
const db = require('./db/knex');
// Funzioni di cleanup (deleteThumbsById, deleteFromDB)
const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup');
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
// recordChange serve per registrare "removed"
const recordChange = require('./api_v1/scanner/recordChange');
const photosRouter = require('./routes/photos');
const wss = require('./ws-server');
const scanFile = require('./api_v1/scanner/scanFileEntry');
const scanPhotoSingle = require('./api_v1/scanner/scanPhotoSingle');
const scanPhotosUser = require('./api_v1/scanner/scanPhotosUser');
const createDeleteFolderFunctions = require('./api_v1/scanner/deleteFolder');
const { deleteFolderForUser } = createDeleteFolderFunctions(db);
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
@ -37,7 +55,6 @@ server.get('/config', (req, res) => {
});
});
// USERS DB
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8'));
@ -72,6 +89,11 @@ function verifyToken(token) {
return jwt.verify(token, SECRET_KEY);
}
function relPath2Cartella(p) {
const parts = p.split("/").filter(Boolean);
return parts.slice(3).join("/");
}
// HOME
server.get('/', (req, res) => {
res.sendFile(path.resolve('public/index.html'));
@ -136,22 +158,41 @@ server.use(/^(?!\/auth).*$/, (req, res, next) => {
}
});
// FILTRO USER (GET)
// ⭐ MIDDLEWARE GET — versione modulare
server.use((req, res, next) => {
if (req.method === 'GET' && req.user && req.user.name !== 'Admin') {
const u = req.user.name;
const q = req.query.user;
const base = q ? (Array.isArray(q) ? q : [q]) : [];
req.query.user = Array.from(new Set([...base, u, 'Common']));
if (req.method === 'GET' && req.user) {
// Admin non viene toccato
if (req.user.name === 'Admin') {
return next();
}
// NON applicare a /scan
if (req.path.startsWith('/scan')) {
req.query.user = [req.user.name];
return next();
}
// Per tutte le altre GET (lettura)
req.query.user = [req.user.name, 'Common'];
}
next();
});
// SCAN FOTO
// SCAN FOTO — Admin scansiona tutti
server.get('/scan', async (req, res) => {
try {
if (req.user && req.user.name === 'Admin') {
await scanPhoto(undefined, 'Admin', db);
// Admin scansiona Common
await scanPhotosUser('Common', db);
// Admin scansiona tutti gli utenti del file users.json
for (const u of userdb.users) {
await scanPhotosUser(u.name, db);
}
return res.send({
status: 'Scansione completata',
user: 'Admin',
@ -159,32 +200,117 @@ server.get('/scan', async (req, res) => {
});
}
await scanPhoto(undefined, req.user.name, db);
// Utente normale → solo se stesso
await scanPhotosUser(req.user.name, db);
res.send({
status: 'Scansione completata',
user: req.user.name,
scope: 'utente corrente',
});
} catch (err) {
console.error('Errore scan:', err);
res.status(500).json({ error: 'Errore durante lo scan', details: err.message });
}
});
// scan automatico
server.post("/api_v1/auto_scan", async (req, res) => {
const { type, file, path, user } = req.body;
// scan_auto new
server.post('/api_v1/auto_scan', async (req, res) => {
const { type, file, path: relPath, user } = req.body;
const cart = relPath2Cartella(relPath);
const absFile = path.join(__dirname, WEB_ROOT, relPath, file);
console.log("=== SUPER_SCAN EVENT ===");
console.log("TYPE:", type);
console.log("FILE:", file);
console.log("PATH:", path);
console.log("USER:", user);
console.log("========================");
/*console.log("type: ", type);
console.log("file: ", file);
console.log("relPath: ", relPath);
console.log("user: ", user);
console.log("cart: ", cart);
console.log("absFile: ", absFile);
return res.json({ status: 'OK', action: 'DEL_DIR', folder: "prova" });
*/
try {
switch (type) {
res.json({ ok: true });
// -------------------------
// ADD_DIR
// -------------------------
case 'ADD_DIR': {
const { scanNewCartella } = require('./api_v1/scanner/scanNewCartella');
const result = await scanNewCartella(user, file, db);
wss.broadcastToUser(user, {
type: "add_dir",
folder: file
});
return res.json({ status: 'OK', action: 'ADD_DIR', result });
}
// -------------------------
// DEL (file)
// -------------------------
case 'DEL': {
const e = await scanFile(user, cart, absFile);
await recordChange(db, e.id, user, "removed");
await deleteThumbsById(e.id);
await deleteFromDB(e.id, user);
wss.broadcastToUser(user, {
type: "removed",
id: e.id
});
return res.json({ status: 'OK', action: 'DEL', id: e.id });
}
// -------------------------
// ADD (file)
// -------------------------
case 'ADD': {
const f = await scanFile(user, cart, absFile);
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
// -------------------------
case 'DEL_DIR': {
await deleteFolderForUser(user, file);
wss.broadcastToUser(user, {
type: "del_dir",
folder: file
});
return res.json({ status: 'OK', action: 'DEL_DIR', folder: cart });
}
default:
return res.status(400).json({ error: 'Tipo non valido' });
}
} catch (err) {
console.error('Errore scan_auto:', err);
res.status(500).json({ error: err.message });
}
});
// FILE STATICI
server.get('/files', (req, res) => {
@ -287,6 +413,11 @@ server.listen(PORT, () => {
console.log(`Auth API server running on port ${PORT} ...`);
});
// ===============================
//
// ===============================
// Pulizia denylist
setInterval(() => {
const nowSec = Math.floor(Date.now() / 1000);

View file

@ -8,12 +8,30 @@ const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const scanPhoto = require('./api_v1/scanner/scanPhoto.js');
const { handleAddFile, handleDeleteFile, handleAddDir, handleDeleteDir } = require('./api_v1/scanner/scanAuto.js');
const { WEB_ROOT } = require('./api_v1/config');
const config = require('./api_v1/config');
const db = require('./db/knex');
// Funzioni di cleanup (deleteThumbsById, deleteFromDB)
const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup');
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
// recordChange serve per registrare "removed"
const recordChange = require('./api_v1/scanner/recordChange');
const photosRouter = require('./routes/photos');
const wss = require('./ws-server');
const scanFile = require('./api_v1/scanner/scanFileEntry');
const scanPhotoSingle = require('./api_v1/scanner/scanPhotoSingle');
const scanPhotosUser = require('./api_v1/scanner/scanPhotosUser');
const createDeleteFolderFunctions = require('./api_v1/scanner/deleteFolder');
const { deleteFolderForUser } = createDeleteFolderFunctions(db);
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
@ -37,7 +55,6 @@ server.get('/config', (req, res) => {
});
});
// USERS DB
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8'));
@ -72,6 +89,11 @@ function verifyToken(token) {
return jwt.verify(token, SECRET_KEY);
}
function relPath2Cartella(p) {
const parts = p.split("/").filter(Boolean);
return parts.slice(3).join("/");
}
// HOME
server.get('/', (req, res) => {
res.sendFile(path.resolve('public/index.html'));
@ -136,22 +158,41 @@ server.use(/^(?!\/auth).*$/, (req, res, next) => {
}
});
// FILTRO USER (GET)
// ⭐ MIDDLEWARE GET — versione modulare
server.use((req, res, next) => {
if (req.method === 'GET' && req.user && req.user.name !== 'Admin') {
const u = req.user.name;
const q = req.query.user;
const base = q ? (Array.isArray(q) ? q : [q]) : [];
req.query.user = Array.from(new Set([...base, u, 'Common']));
if (req.method === 'GET' && req.user) {
// Admin non viene toccato
if (req.user.name === 'Admin') {
return next();
}
// NON applicare a /scan
if (req.path.startsWith('/scan')) {
req.query.user = [req.user.name];
return next();
}
// Per tutte le altre GET (lettura)
req.query.user = [req.user.name, 'Common'];
}
next();
});
// SCAN FOTO
// SCAN FOTO — Admin scansiona tutti
server.get('/scan', async (req, res) => {
try {
if (req.user && req.user.name === 'Admin') {
await scanPhoto(undefined, 'Admin', db);
// Admin scansiona Common
await scanPhotosUser('Common', db);
// Admin scansiona tutti gli utenti del file users.json
for (const u of userdb.users) {
await scanPhotosUser(u.name, db);
}
return res.send({
status: 'Scansione completata',
user: 'Admin',
@ -159,32 +200,157 @@ server.get('/scan', async (req, res) => {
});
}
await scanPhoto(undefined, req.user.name, db);
// Utente normale → solo se stesso
await scanPhotosUser(req.user.name, db);
res.send({
status: 'Scansione completata',
user: req.user.name,
scope: 'utente corrente',
});
} catch (err) {
console.error('Errore scan:', err);
res.status(500).json({ error: 'Errore durante lo scan', details: err.message });
}
});
// scan automatico
server.post("/api_v1/auto_scan", async (req, res) => {
const { type, file, path, user } = req.body;
// scan_auto new
server.post('/api_v1/auto_scan', async (req, res) => {
const { type, file, path: relPath, user } = req.body;
const cart = relPath2Cartella(relPath);
const absFile = path.join(__dirname, WEB_ROOT, relPath, file);
/*console.log("type: ", type);
console.log("file: ", file);
console.log("relPath: ", relPath);
console.log("user: ", user);
console.log("cart: ", cart);
console.log("absFile: ", absFile);
return res.json({ status: 'OK', action: 'DEL_DIR', folder: "prova" });
*/
try {
switch (type) {
// -------------------------
// ADD_DIR
// -------------------------
case 'ADD_DIR': {
const { scanNewCartella } = require('./api_v1/scanner/scanNewCartella');
const result = await scanNewCartella(user, file, db);
wss.broadcastToUser(user, {
type: "add_dir",
folder: file
});
return res.json({ status: 'OK', action: 'ADD_DIR', result });
}
// -------------------------
// DEL (file)
// -------------------------
case 'DEL': {
const e = await scanFile(user, cart, absFile);
await recordChange(db, e.id, user, "removed");
await deleteThumbsById(e.id);
await deleteFromDB(e.id, user);
wss.broadcastToUser(user, {
type: "removed",
id: e.id
});
return res.json({ status: 'OK', action: 'DEL', id: e.id });
}
// -------------------------
// ADD (file)
// -------------------------
case 'ADD': {
const f = await scanFile(user, cart, absFile);
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
// -------------------------
case 'DEL_DIR': {
await deleteFolderForUser(user, file);
wss.broadcastToUser(user, {
type: "del_dir",
folder: file
});
return res.json({ status: 'OK', action: 'DEL_DIR', folder: cart });
}
default:
return res.status(400).json({ error: 'Tipo non valido' });
}
} catch (err) {
console.error('Errore scan_auto:', err);
res.status(500).json({ error: err.message });
}
});
// scan automatico
server.post("/api_v1/auto_scan2", async (req, res) => {
const { type, file, path: relPath, user } = req.body;
const cart = relPath2Cartella(relPath);
const absFile = path.join(__dirname,WEB_ROOT,relPath,file);
const e = await scanFile(user, cart, absFile);
const exists = await db('photos')
.where({ id: e.id })
.select(db.raw('1'))
.first();
const found = !!exists;
console.log("Trovato: ", found);
console.log("=== SUPER_SCAN EVENT ===");
console.log("TYPE:", type);
console.log("FILE:", file);
console.log("PATH:", path);
console.log("PATH:", relPath);
console.log("CART", cart);
console.log("ABSPATH:", __dirname);
console.log("USER:", user);
console.log("WEB_ROOT:", WEB_ROOT);
console.log("absFike: ", absFile);
console.log(e);
console.log("========================");
res.json({ ok: true });
});
server.post("/api_v1/auto_scan1", async (req, res) => {
const { type, file, path: relPath, user } = req.body;
const cart = relPath2Cartella(relPath);
const absFile = path.join(__dirname,WEB_ROOT,relPath,file);
const f = await scanFile(user, cart, absFile);
const newFiles = [];
await scanPhotoSingle(db, user, cart, f, newFiles);
console.log("Nuovo file", newFiles);
res.json({ ok: true });
});
// FILE STATICI
server.get('/files', (req, res) => {
@ -287,6 +453,11 @@ server.listen(PORT, () => {
console.log(`Auth API server running on port ${PORT} ...`);
});
// ===============================
//
// ===============================
// Pulizia denylist
setInterval(() => {
const nowSec = Math.floor(Date.now() / 1000);

8
w.sh
View file

@ -1,8 +0,0 @@
#!/bin/bash
WATCH_DIR="./public/photos/Fabio"
echo "Monitoro con inotifywait: $WATCH_DIR"
# Avvia Node e gli passa gli eventi
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" | node watcher_logic1.mjs Fabio

22
w3.sh
View file

@ -1,22 +0,0 @@
#!/bin/bash
USERS_FILE="./api_v1/users.json"
# Legge ogni utente dal JSON
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
# Determina la cartella da monitorare
if [[ "$user" == "Admin" ]]; then
WATCH_DIR="./public/photos/Common/original"
else
WATCH_DIR="./public/photos/$user/original"
fi
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
# Avvia watcher per ogni utente in background
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
| node watcher_logic3.mjs "$user" &
done
wait

22
w4.sh
View file

@ -1,22 +0,0 @@
#!/bin/bash
USERS_FILE="./api_v1/users.json"
# Legge ogni utente dal JSON
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
# Determina la cartella da monitorare
if [[ "$user" == "Admin" ]]; then
WATCH_DIR="./public/photos/Common/original"
else
WATCH_DIR="./public/photos/$user/original"
fi
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
# Avvia watcher per ogni utente in background
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
| node watcher_logic4.mjs "$user" &
done
wait

View file

@ -1,39 +0,0 @@
import fs from "fs";
import readline from "readline";
const user = process.argv[2];
const LOCK_FILE = `./${user}.scan`;
const DELAY = 10000;
let lastEventTime = 0;
let timer = null;
console.log(`Node attivo per utente: ${user}`);
function handleQuietPeriod() {
const now = Date.now();
if (now - lastEventTime < DELAY) return;
if (fs.existsSync(LOCK_FILE)) {
console.log(`Lock ${LOCK_FILE} presente, attendo...`);
timer = setTimeout(handleQuietPeriod, DELAY);
return;
}
console.log(`modificato ${user} (nessuna attività per 10 secondi)`);
fs.writeFileSync(LOCK_FILE, "lock");
}
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity
});
rl.on("line", line => {
console.log("Evento:", line);
lastEventTime = Date.now();
if (timer) clearTimeout(timer);
timer = setTimeout(handleQuietPeriod, DELAY);
});

View file

@ -1,35 +0,0 @@
import fs from "fs";
import readline from "readline";
const user = process.argv[2];
// Estensioni foto/video
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
// Funzione per capire se è foto o video
function isMediaFile(filename) {
const lower = filename.toLowerCase();
return photoExt.some(ext => lower.endsWith(ext)) ||
videoExt.some(ext => lower.endsWith(ext));
}
console.log(`Node attivo per utente: ${user}`);
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity
});
rl.on("line", line => {
// line contiene: "path ACTION file"
const parts = line.trim().split(" ");
const file = parts[parts.length - 1];
const path = parts[0];
if (isMediaFile(file)) {
console.log(`Scan di ${user}: ${path}${file}`);
} else {
console.log(`Ignorato (non media): ${file}`);
}
});

View file

@ -1,64 +0,0 @@
import readline from "readline";
const user = process.argv[2];
// Estensioni foto/video
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
function isMediaFile(filename) {
const lower = filename.toLowerCase();
return photoExt.some(ext => lower.endsWith(ext)) ||
videoExt.some(ext => lower.endsWith(ext));
}
console.log(`Node attivo per utente: ${user}`);
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity
});
rl.on("line", line => {
// Esempio: "/path/dir/ CLOSE_WRITE,CLOSE file.jpg"
const parts = line.trim().split(/\s+/);
const path = parts[0];
const action = parts[1];
const file = parts[2];
if (!file) return;
if (!isMediaFile(file)) {
console.log(`Ignorato (non media): ${file}`);
return;
}
let type = null;
// ADD → solo CLOSE_WRITE o CLOSE_WRITE,CLOSE
if (/^CLOSE_WRITE(,CLOSE)?$/.test(action)) {
type = "ADD";
}
// ADD → MOVED_TO (solo se esatto)
else if (action === "MOVED_TO") {
type = "ADD";
}
// DEL → MOVED_FROM
else if (action === "MOVED_FROM") {
type = "DEL";
}
// DEL → DELETE
else if (action === "DELETE") {
type = "DEL";
}
else {
return;
}
console.log(`${type} ${file} ${path} ${user}`);
});

View file

@ -1,84 +0,0 @@
import readline from "readline";
const user = process.argv[2];
// Estensioni foto/video
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
function isMediaFile(filename) {
const lower = filename.toLowerCase();
return photoExt.some(ext => lower.endsWith(ext)) ||
videoExt.some(ext => lower.endsWith(ext));
}
console.log(`Node attivo per utente: ${user}`);
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity
});
rl.on("line", line => {
const parts = line.trim().split(/\s+/);
const path = parts[0];
const action = parts[1];
const file = parts[2];
if (!file) return;
const isDir = action.includes("ISDIR");
// 🔥 Se è directory → NON controllare mediafile
if (!isDir && !isMediaFile(file)) {
console.log(`Ignorato (non media): ${file}`);
return;
}
let type = null;
//
// 🔵 DIRECTORY EVENTS
//
// ADD_DIR → MOVED_TO,ISDIR
if (action === "MOVED_TO,ISDIR") {
type = "ADD_DIR";
}
// DEL_DIR → MOVED_FROM,ISDIR
else if (action === "MOVED_FROM,ISDIR") {
type = "DEL_DIR";
}
//
// 🔵 FILE EVENTS
//
// ADD → CLOSE_WRITE o CLOSE_WRITE,CLOSE
else if (/^CLOSE_WRITE(,CLOSE)?$/.test(action)) {
type = "ADD";
}
// ADD → MOVED_TO
else if (action === "MOVED_TO") {
type = "ADD";
}
// DEL → MOVED_FROM
else if (action === "MOVED_FROM") {
type = "DEL";
}
// DEL → DELETE
else if (action === "DELETE") {
type = "DEL";
}
else {
return;
}
console.log(`${type} ${file} ${path} ${user}`);
});

View file

@ -1,154 +0,0 @@
import readline from "readline";
import fs from "fs";
import dotenv from "dotenv";
dotenv.config();
const user = process.argv[2];
// ===============================
// BASE_URL DAL .env
// ===============================
const BASE_URL = process.env.BASE_URL; // es: https://prova.patachina.it
if (!BASE_URL) {
console.error("ERRORE: BASE_URL non definita in .env");
process.exit(1);
}
// ===============================
// CREDENZIALI ADMIN
// ===============================
const adminSecret = JSON.parse(fs.readFileSync("./api_v1/admin_secret.json", "utf8"));
let adminToken = null;
let isRefreshing = false;
// ===============================
// LOGIN ADMIN (fetch come frontend)
// ===============================
async function loginAdmin() {
const res = await fetch(`${BASE_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: adminSecret.email,
password: adminSecret.password
})
});
if (!res.ok) {
console.error("Errore login Admin:", await res.text());
throw new Error("Login fallito");
}
const data = await res.json();
adminToken = data.token;
console.log("Watcher autenticato come Admin");
}
// ===============================
// GARANTISCE TOKEN VALIDO
// ===============================
async function ensureAdminToken() {
if (adminToken) return;
if (isRefreshing) {
return new Promise(resolve => {
const interval = setInterval(() => {
if (!isRefreshing) {
clearInterval(interval);
resolve();
}
}, 100);
});
}
isRefreshing = true;
await loginAdmin();
isRefreshing = false;
}
// ===============================
// CHIAMATA A /auto_scan (fetch come frontend)
// ===============================
async function callAutoScan(type, file, path, user) {
await ensureAdminToken();
const res = await fetch(`${BASE_URL}/api_v1/auto_scan`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + adminToken
},
body: JSON.stringify({ type, file, path, user })
});
if (res.status === 401) {
console.log("Token scaduto, rinnovo…");
adminToken = null;
return callAutoScan(type, file, path, user);
}
return res.ok;
}
// ===============================
// ESTENSIONI MEDIA
// ===============================
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
function isMediaFile(filename) {
const lower = filename.toLowerCase();
return photoExt.some(ext => lower.endsWith(ext)) ||
videoExt.some(ext => lower.endsWith(ext));
}
console.log(`Node attivo per utente: ${user}`);
const rl = readline.createInterface({
input: process.stdin,
crlfDelay: Infinity
});
// ===============================
// LOGICA EVENTI
// ===============================
function startWatcher() {
rl.on("line", line => {
const parts = line.trim().split(/\s+/);
const path = parts[0];
const action = parts[1];
const file = parts[2];
if (!file) return;
const isDir = action.includes("ISDIR");
if (!isDir && !isMediaFile(file)) {
console.log(`Ignorato (non media): ${file}`);
return;
}
let type = null;
if (action === "MOVED_TO,ISDIR") type = "ADD_DIR";
else if (action === "MOVED_FROM,ISDIR") type = "DEL_DIR";
else if (/^CLOSE_WRITE(,CLOSE)?$/.test(action)) type = "ADD";
else if (action === "MOVED_TO") type = "ADD";
else if (action === "MOVED_FROM") type = "DEL";
else if (action === "DELETE") type = "DEL";
else return;
console.log(`${type} ${file} ${path} ${user}`);
callAutoScan(type, file, path, user);
});
}
// ===============================
// AVVIO: LOGIN + WATCHER
// ===============================
loginAdmin().then(() => {
startWatcher();
});

22
ww.sh
View file

@ -1,22 +0,0 @@
#!/bin/bash
USERS_FILE="./api_v1/users.json"
# Legge ogni utente dal JSON
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
# Determina la cartella da monitorare
if [[ "$user" == "Admin" ]]; then
WATCH_DIR="./public/photos/Common/original"
else
WATCH_DIR="./public/photos/$user/original"
fi
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
# Avvia watcher per ogni utente in background
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
| node watcher_logic1.mjs "$user" &
done
wait

22
www.sh
View file

@ -1,22 +0,0 @@
#!/bin/bash
USERS_FILE="./api_v1/users.json"
# Legge ogni utente dal JSON
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
# Determina la cartella da monitorare
if [[ "$user" == "Admin" ]]; then
WATCH_DIR="./public/photos/Common/original"
else
WATCH_DIR="./public/photos/$user/original"
fi
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
# Avvia watcher per ogni utente in background
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
| node watcher_logic2.mjs "$user" &
done
wait