This commit is contained in:
Fabio 2026-03-23 12:31:01 +01:00
parent 5fb8ef25c1
commit 107f6c9609
75 changed files with 27732 additions and 4131 deletions

1
.Fabio.last_event Normal file
View file

@ -0,0 +1 @@
1774133507738

21
.env
View file

@ -1,12 +1,29 @@
BASE_URL=https://prova.patachina.it BASE_URL=https://prova.patachina.it
SERVER_PORT=4000 SERVER_PORT=4000
EMAIL=fabio@gmail.com EMAIL=fabio@gmail.com
PASSWORD=master66 PASSWORD=master66
JWT_SECRET=123456789 JWT_SECRET=123456789
JWT_EXPIRES=1h JWT_EXPIRES=1h
# Dove si trova la cartella public (relativa alla root del progetto)
WEB_ROOT=public
# Percorso relativo di index.json dentro public/
INDEX_PATH=photos/index.json
# true = restituisce path assoluti nei record
PATH_FULL=true PATH_FULL=true
LOG_MODE=both # console | file | both
LOG_FILE=scan.log # nome file log # Logging
LOG_MODE=both # console | file | both
LOG_FILE=scan.log
LOG_DIR=public LOG_DIR=public
LOG_VERBOSE=true LOG_VERBOSE=true
GALLERY_REFRESH_SECONDS=30
WS_PORT=4002
WS_HOST=0.0.0.0

1
Fabio.scan Normal file
View file

@ -0,0 +1 @@
lock

1
a.jpg Normal file
View file

@ -0,0 +1 @@
prova

4
api_v1/admin_secret.json Normal file
View file

@ -0,0 +1,4 @@
{
"email": "admin@gmail.com",
"password": "master66"
}

View file

@ -1,17 +1,25 @@
require('dotenv').config(); require('dotenv').config();
const path = require('path');
module.exports = { module.exports = {
BASE_URL: process.env.BASE_URL, BASE_URL: process.env.BASE_URL,
EMAIL: process.env.EMAIL, EMAIL: process.env.EMAIL,
PASSWORD: process.env.PASSWORD, PASSWORD: process.env.PASSWORD,
SEND_PHOTOS: (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true', SEND_PHOTOS: (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true',
WRITE_INDEX: (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true',
// Cartella public (root dei file statici)
WEB_ROOT: process.env.WEB_ROOT || 'public', WEB_ROOT: process.env.WEB_ROOT || 'public',
// PATH_FULL ora è un boolean, non un path
PATH_FULL: (process.env.PATH_FULL || 'false').toLowerCase() === 'true', PATH_FULL: (process.env.PATH_FULL || 'false').toLowerCase() === 'true',
INDEX_PATH: process.env.INDEX_PATH || path.posix.join('photos', 'index.json'),
// Estensioni supportate
SUPPORTED_EXTS: new Set([ SUPPORTED_EXTS: new Set([
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif', '.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
'.mp4', '.mov', '.m4v' '.mp4', '.mov', '.m4v'
]) ]),
// 🔥 NUOVO: intervallo refresh gallery (in secondi)
GALLERY_REFRESH_SECONDS: Number(process.env.GALLERY_REFRESH_SECONDS || 30)
}; };

BIN
api_v1/database.sqlite Normal file

Binary file not shown.

View file

@ -1 +0,0 @@
{"photos":[]}

View file

@ -1,33 +0,0 @@
// scanner/indexStore.js
const path = require('path');
const fsp = require('fs/promises');
const { WEB_ROOT, INDEX_PATH } = require('../config');
const absIndexPath = path.resolve(__dirname, '..', '..', WEB_ROOT, INDEX_PATH);
const absIndexTmp = absIndexPath + '.tmp';
const PRETTY = process.env.INDEX_PRETTY === 'true';
async function loadPreviousIndex() {
try {
const raw = await fsp.readFile(absIndexPath, 'utf8');
const json = JSON.parse(raw);
return (json && typeof json === 'object') ? json : {};
} catch {
return {};
}
}
async function saveIndex(indexTree) {
const dir = path.dirname(absIndexPath);
await fsp.mkdir(dir, { recursive: true });
const data = PRETTY
? JSON.stringify(indexTree, null, 2)
: JSON.stringify(indexTree);
await fsp.writeFile(absIndexTmp, data, 'utf8');
await fsp.rename(absIndexTmp, absIndexPath);
}
module.exports = { loadPreviousIndex, saveIndex, absIndexPath };

View file

@ -1,31 +0,0 @@
// scanner/logger.js
const fs = require('fs');
const path = require('path');
const LOG_MODE = process.env.LOG_MODE || "console"; // console | file | both
const LOG_FILE = process.env.LOG_FILE || "scan.log";
let stream = null;
if (LOG_MODE === "file" || LOG_MODE === "both") {
const logPath = path.resolve(__dirname, "..", LOG_FILE);
stream = fs.createWriteStream(logPath, { flags: "a" });
}
function ts() {
return new Date().toISOString().replace("T", " ").split(".")[0];
}
function log(message) {
const line = `${ts()} ${message}\n`;
if (LOG_MODE === "console" || LOG_MODE === "both") {
process.stdout.write(line);
}
if (LOG_MODE === "file" || LOG_MODE === "both") {
stream.write(line);
}
}
module.exports = { log };

View file

@ -1,62 +1,66 @@
// scanner/orphanCleanup.js // api_v1/scanner/orphanCleanup.js
const fs = require('fs');
const fsp = require('fs/promises'); const fsp = require('fs/promises');
const path = require('path'); const path = require('path');
const { WEB_ROOT, INDEX_PATH } = require('../config');
module.exports = function createCleanupFunctions(db) { module.exports = function createCleanupFunctions(db) {
// 1) Recupera gli ID dal DB per una cartella specifica
async function buildIdsListForFolder(userName, cartella) { async function buildIdsListForFolder(userName, cartella) {
const indexPath = path.resolve(__dirname, '..', '..', WEB_ROOT, INDEX_PATH);
const idsIndex = [];
if (!fs.existsSync(indexPath)) return idsIndex;
try { try {
const raw = await fsp.readFile(indexPath, 'utf8'); const rows = await db("photos")
const index = JSON.parse(raw); .select("id")
.where({ user: userName, cartella });
const folder = index?.[userName]?.[cartella]; return rows.map(r => r.id);
if (!folder) return idsIndex; } catch (err) {
console.error("Errore buildIdsListForFolder:", err);
return Object.keys(folder).filter(k => k !== "_folderHash"); return [];
} catch {
return idsIndex;
} }
} }
// 2) Rimuove un ID dalla lista (invariati)
function removeIdFromList(idsIndex, id) { function removeIdFromList(idsIndex, id) {
return idsIndex.filter(x => x !== id); return idsIndex.filter(x => x !== id);
} }
// 3) Cancella thumbs dal filesystem usando SQLite
async function deleteThumbsById(id) { async function deleteThumbsById(id) {
const col = db.get('photos'); const rec = await db('photos').where({ id }).first();
const rec = col.find({ id }).value();
if (!rec) return false; if (!rec) return false;
const thumbs = [rec.thub1, rec.thub2].filter(Boolean); const thumbs = [rec.thub1, rec.thub2].filter(Boolean);
let deleted = false; let deleted = false;
for (const t of thumbs) { for (const t of thumbs) {
const abs = path.resolve(__dirname, '../public' + t); const abs = path.resolve(__dirname, '..', '..', 'public', t);
if (fs.existsSync(abs)) { try {
await fsp.rm(abs, { force: true }); await fsp.rm(abs, { force: true });
console.log(` 🔴 Thumb eliminato: ${abs}`); console.log(` 🔴 Thumb eliminato: ${abs}`);
deleted = true; deleted = true;
} } catch {}
} }
return deleted; return deleted;
} }
function deleteFromDB(id) { // 4) Cancella dal DB SQLite + registra in photo_changes
const col = db.get('photos'); async function deleteFromDB(id, userName) {
const exists = col.find({ id }).value(); const deleted = await db('photos').where({ id }).del();
if (exists) { if (deleted > 0) {
col.remove({ id }).write();
console.log(` 🔴 Eliminato dal DB: ${id}`); console.log(` 🔴 Eliminato dal DB: ${id}`);
// 🔥 REGISTRAZIONE IN photo_changes
await db('photo_changes').insert({
photo_id: id,
user: userName,
change_type: 'removed',
timestamp: new Date().toISOString()
});
return true; return true;
} }
return false; return false;
} }

View file

@ -1,60 +1,24 @@
const axios = require('axios'); // scanner/postWithAuth.js
const { BASE_URL, EMAIL, PASSWORD, SEND_PHOTOS } = require('../config'); // Versione locale: niente HTTP, niente token, solo DB
let cachedToken = null; module.exports = function createPostToDB(db) {
async function getToken(force = false) { /**
if (!SEND_PHOTOS) return null; * Inserisce o aggiorna un record nel DB SQLite
if (cachedToken && !force) return cachedToken; * (sostituisce completamente axios.post)
*/
try { async function postToDB(record) {
const res = await axios.post(`${BASE_URL}/auth/login`, { if (!record || !record.id) {
email: EMAIL, throw new Error("Record non valido");
password: PASSWORD
});
cachedToken = res.data.token;
return cachedToken;
} catch (err) {
console.error('ERRORE LOGIN:', err.message);
return null;
}
}
async function postWithAuth(url, payload) {
if (!SEND_PHOTOS) return;
let token = await getToken();
if (!token) throw new Error('Token assente');
try {
await axios.post(url, payload, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 20000,
});
} catch (err) {
if (err.response && err.response.status === 401) {
token = await getToken(true);
if (!token) throw err;
await axios.post(url, payload, {
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
},
timeout: 20000,
});
} else {
throw err;
} }
await db('photos')
.insert(record)
.onConflict('id')
.merge();
return true;
} }
}
module.exports = postWithAuth;
return postToDB;
};

View file

@ -1,171 +0,0 @@
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');
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 });
const baseName = path.parse(fileRelPath).name;
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 {}
const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
const takenAtIso = parseExifDateUtc(timeRaw);
// --- GPS ---
let gps = isVideo
? await extractGpsWithExiftool(absPath)
: extractGpsFromExif(tags);
// --- DIMENSIONI & ROTAZIONE ---
let width = null, height = null, duration = 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;
// --- ROTAZIONE VIDEO (robusta, compatibile Xiaomi) ---
rotation = 0;
// 1) rotate standard
if (stream?.tags?.rotate) {
rotation = Number(stream.tags.rotate);
}
// 2) side_data_list rotation
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);
}
// 3) Xiaomi: displaymatrix string
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]);
}
}
}
// Normalizza (Xiaomi usa -90 → 270)
rotation = ((rotation % 360) + 360) % 360;
}
duration = info.format?.duration || null;
} else {
// FOTO
try {
const meta = await sharp(absPath).metadata();
width = meta.width || null;
height = meta.height || null;
} catch {}
// --- ROTAZIONE EXIF FOTO ---
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,
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: isVideo ? duration : null,
location
};
}
module.exports = processFile;

View file

@ -1,171 +0,0 @@
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');
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 });
const baseName = path.parse(fileRelPath).name;
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 {}
const timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
const takenAtIso = parseExifDateUtc(timeRaw);
// --- GPS ---
let gps = isVideo
? await extractGpsWithExiftool(absPath)
: extractGpsFromExif(tags);
// --- 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;
// --- ROTAZIONE VIDEO (robusta, compatibile Xiaomi) ---
rotation = 0;
// 1) rotate standard
if (stream?.tags?.rotate) {
rotation = Number(stream.tags.rotate);
}
// 2) side_data_list rotation
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);
}
// 3) Xiaomi: displaymatrix string
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]);
}
}
}
// Normalizza (Xiaomi usa -90 → 270)
rotation = ((rotation % 360) + 360) % 360;
}
duration = info.format?.duration || null;
duration_ms = duration ? Math.round(duration * 1000) : null;
} else {
// FOTO
try {
const meta = await sharp(absPath).metadata();
width = meta.width || null;
height = meta.height || null;
} catch {}
// --- ROTAZIONE EXIF FOTO ---
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,
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: isVideo ? duration : null,
duration_ms: isVideo ? duration_ms : null,
location
};
}
module.exports = processFile;

View file

@ -1,11 +1,20 @@
// scanner/scanCartella.js // api_v1/scanner/scanCartella.js
const path = require('path'); const path = require('path');
const fsp = require('fs/promises'); const fsp = require('fs/promises');
const processFile = require('./processFile');
const { sha256 } = require('./utils'); const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config'); const { SUPPORTED_EXTS } = require('../config');
async function* scanCartella(userName, cartella, absCartella, previousIndexTree) { /**
* 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 = '') { async function* walk(currentAbs, relPath = '') {
let entries = []; let entries = [];
@ -27,40 +36,25 @@ async function* scanCartella(userName, cartella, absCartella, previousIndexTree)
if (!SUPPORTED_EXTS.has(ext)) continue; if (!SUPPORTED_EXTS.has(ext)) continue;
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name; const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
// ID deterministico (stesso criterio di prima)
const id = sha256(`${userName}/${cartella}/${fileRelPath}`); const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
// Stat veloce
let st; let st;
try { st = await fsp.stat(absPath); } catch { continue; } try { st = await fsp.stat(absPath); } catch { continue; }
const hash = sha256(`${st.size}-${st.mtimeMs}`); yield {
const prev = previousIndexTree?.[userName]?.[cartella]?.[id]; id,
user: userName,
if (prev && prev.hash === hash) {
yield {
id,
user: userName,
cartella,
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`,
_indexHash: hash,
unchanged: true
};
continue;
}
const meta = await processFile(
userName,
cartella, cartella,
fileRelPath, name: e.name,
relPath: fileRelPath,
absPath, absPath,
ext, ext,
st stat: st,
); path: `/photos/${userName}/original/${cartella}/${fileRelPath}`
};
meta.id = id;
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
meta._indexHash = hash;
yield meta;
} }
} }

View file

@ -1,72 +0,0 @@
// scanner/scanCartella.js
const path = require('path');
const fsp = require('fs/promises');
const processFile = require('./processFile');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
async function* scanCartella(userName, cartella, absCartella, previousIndexTree) {
async function* walk(currentAbs, relPath = '') {
let entries = [];
try {
entries = await fsp.readdir(currentAbs, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const absPath = path.join(currentAbs, e.name);
if (e.isDirectory()) {
yield* walk(absPath, path.join(relPath, e.name));
continue;
}
const ext = path.extname(e.name).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) continue;
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
let st;
try { st = await fsp.stat(absPath); } catch { continue; }
const hash = sha256(`${st.size}-${st.mtimeMs}`);
const prev = previousIndexTree?.[userName]?.[cartella]?.[id];
// FILE INVARIATO
if (prev && prev.hash === hash) {
yield {
id,
user: userName,
cartella,
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`,
_indexHash: hash,
unchanged: true
};
continue;
}
// FILE NUOVO/MODIFICATO
const meta = await processFile(
userName,
cartella,
fileRelPath,
absPath,
ext,
st
);
meta.id = id;
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
meta._indexHash = hash;
yield meta;
}
}
yield* walk(absCartella);
}
module.exports = scanCartella;

View file

@ -1,71 +0,0 @@
// scanner/scanCartella.js
const path = require('path');
const fsp = require('fs/promises');
const processFile = require('./processFile');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
const { log } = require('./logger');
async function* scanCartella(userName, cartella, absCartella, previousIndexTree) {
async function* walk(currentAbs, relPath = '') {
let entries = [];
try {
entries = await fsp.readdir(currentAbs, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const absPath = path.join(currentAbs, e.name);
if (e.isDirectory()) {
yield* walk(absPath, path.join(relPath, e.name));
continue;
}
const ext = path.extname(e.name).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) continue;
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
let st;
try { st = await fsp.stat(absPath); } catch { continue; }
const hash = sha256(`${st.size}-${st.mtimeMs}`);
const prev = previousIndexTree?.[userName]?.[cartella]?.[id];
if (prev && prev.hash === hash) {
yield {
id,
user: userName,
cartella,
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`,
_indexHash: hash,
unchanged: true
};
continue;
}
const meta = await processFile(
userName,
cartella,
fileRelPath,
absPath,
ext,
st
);
meta.id = id;
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
meta._indexHash = hash;
yield meta;
}
}
yield* walk(absCartella);
}
module.exports = scanCartella;

View file

@ -1,70 +0,0 @@
// scanner/scanCartella.js
const path = require('path');
const fsp = require('fs/promises');
const processFile = require('./processFile');
const { sha256 } = require('./utils');
const { SUPPORTED_EXTS } = require('../config');
async function* scanCartella(userName, cartella, absCartella, previousIndexTree) {
async function* walk(currentAbs, relPath = '') {
let entries = [];
try {
entries = await fsp.readdir(currentAbs, { withFileTypes: true });
} catch {
return;
}
for (const e of entries) {
const absPath = path.join(currentAbs, e.name);
if (e.isDirectory()) {
yield* walk(absPath, path.join(relPath, e.name));
continue;
}
const ext = path.extname(e.name).toLowerCase();
if (!SUPPORTED_EXTS.has(ext)) continue;
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
let st;
try { st = await fsp.stat(absPath); } catch { continue; }
const hash = sha256(`${st.size}-${st.mtimeMs}`);
const prev = previousIndexTree?.[userName]?.[cartella]?.[id];
if (prev && prev.hash === hash) {
yield {
id,
user: userName,
cartella,
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`,
_indexHash: hash,
unchanged: true
};
continue;
}
const meta = await processFile(
userName,
cartella,
fileRelPath,
absPath,
ext,
st
);
meta.id = id;
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
meta._indexHash = hash;
yield meta;
}
}
yield* walk(absCartella);
}
module.exports = scanCartella;

View file

@ -1,26 +1,24 @@
// scanner/scanPhoto.js // api_v1/scanner/scanPhoto.js
const path = require('path'); const path = require('path');
const fsp = require('fs/promises'); const fsp = require('fs/promises');
const { log } = require('./logger'); const { log } = require('./logger');
const { loadPreviousIndex, saveIndex } = require('./indexStore');
const scanCartella = require('./scanCartella'); const scanCartella = require('./scanCartella');
const processFile = require('./processFile');
const postWithAuth = require('./postWithAuth'); const postWithAuth = require('./postWithAuth');
const { sha256 } = require('./utils');
const { const {
WEB_ROOT, WEB_ROOT,
SEND_PHOTOS, SEND_PHOTOS,
BASE_URL, BASE_URL,
WRITE_INDEX,
SUPPORTED_EXTS SUPPORTED_EXTS
} = require('../config'); } = require('../config');
const createCleanupFunctions = require('./orphanCleanup'); const createCleanupFunctions = require('./orphanCleanup');
// Variabile per log completo o ridotto
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true"); const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
// Formatta ms → HH:MM:SS
function formatTime(ms) { function formatTime(ms) {
const sec = Math.floor(ms / 1000); const sec = Math.floor(ms / 1000);
const h = String(Math.floor(sec / 3600)).padStart(2, '0'); const h = String(Math.floor(sec / 3600)).padStart(2, '0');
@ -29,9 +27,6 @@ function formatTime(ms) {
return `${h}:${m}:${s}`; return `${h}:${m}:${s}`;
} }
// ---------------------------------------------------------
// ⚡ CONTEGGIO FILE VELOCE (senza hashing, EXIF, sharp, ecc.)
// ---------------------------------------------------------
async function countFilesFast(rootDir) { async function countFilesFast(rootDir) {
let count = 0; let count = 0;
@ -65,30 +60,14 @@ async function scanPhoto(dir, userName, db) {
const start = Date.now(); const start = Date.now();
log(`🔵 Inizio scan globale per user=${userName}`); log(`🔵 Inizio scan globale per user=${userName}`);
const previousIndexTree = await loadPreviousIndex(); const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
const {
buildIdsListForFolder,
removeIdFromList,
deleteThumbsById,
deleteFromDB
} = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos'); const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original'); const userDir = path.join(photosRoot, userName, 'original');
let totalNew = 0;
let totalDeleted = 0;
let totalUnchanged = 0;
let newFiles = []; let newFiles = [];
// ---------------------------------------------------------
// 1) ⚡ CONTEGGIO FILE SUPER VELOCE
// ---------------------------------------------------------
const TOTAL_FILES = await countFilesFast(userDir); const TOTAL_FILES = await countFilesFast(userDir);
if (TOTAL_FILES === 0) { if (TOTAL_FILES === 0) {
log(`Nessun file trovato per user=${userName}`); log(`Nessun file trovato per user=${userName}`);
return []; return [];
@ -98,13 +77,10 @@ async function scanPhoto(dir, userName, db) {
let CURRENT = 0; let CURRENT = 0;
// ---------------------------------------------------------
// Funzione per aggiornare il file statico leggibile dallHTML
// ---------------------------------------------------------
async function updateStatusFile() { async function updateStatusFile() {
const now = Date.now(); const now = Date.now();
const elapsedMs = now - start; const elapsedMs = now - start;
const avg = elapsedMs / CURRENT; const avg = elapsedMs / Math.max(CURRENT, 1);
const remainingMs = (TOTAL_FILES - CURRENT) * avg; const remainingMs = (TOTAL_FILES - CURRENT) * avg;
const status = { const status = {
@ -120,80 +96,209 @@ async function scanPhoto(dir, userName, db) {
} }
// --------------------------------------------------------- // ---------------------------------------------------------
// 2) SCAN REALE DELLE CARTELLE // SCAN DI UNA SINGOLA CARTELLA
// --------------------------------------------------------- // ---------------------------------------------------------
async function scanSingleFolder(user, cartella, absCartella) { async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`); log(`📁 Inizio cartella: ${cartella}`);
let idsIndex = await buildIdsListForFolder(user, cartella); const rows = await db('photos')
.where({ user, cartella })
.select('id');
let newCount = 0; const idsSet = new Set(rows.map(r => r.id));
let unchangedCount = 0;
let deletedCount = 0;
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) { for await (const f of scanCartella(user, cartella, absCartella, db)) {
CURRENT++; CURRENT++;
const fileName = m.path.split('/').pop();
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
// Aggiorna file statico per lHTML
await updateStatusFile(); 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}`);
// ⚪ FILE INVARIATO
if (m.unchanged) {
if (LOG_VERBOSE) {
log(`${prefix} ⚪ Invariato: ${fileName}`);
}
idsIndex = removeIdFromList(idsIndex, m.id);
unchangedCount++;
continue; continue;
} }
// 🟢 FILE NUOVO O MODIFICATO // ---------------------------------------------------------
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`); // FAST-SIZE-SKIP
// ---------------------------------------------------------
if (prev.size_bytes === st.size) {
//log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
idsSet.delete(id);
nextIndexTree[m.user] ??= {}; await db("photos")
nextIndexTree[m.user][m.cartella] ??= {}; .where({ id })
nextIndexTree[m.user][m.cartella][m.id] = { .update({
id: m.id, path: f.path,
user: m.user, size_bytes: st.size,
cartella: m.cartella, mtimeMs: st.mtimeMs,
path: m.path, fast_hash: fastHash
hash: m._indexHash });
};
idsIndex = removeIdFromList(idsIndex, m.id); continue;
newCount++;
newFiles.push(m);
}
// 🔴 ORFANI
for (const orphanId of idsIndex) {
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
const fileName = old?.path?.split('/').pop() || orphanId;
log(` 🔴 Eliminato: ${fileName}`);
await deleteThumbsById(orphanId);
deleteFromDB(orphanId);
const userTree = nextIndexTree[user];
if (userTree?.[cartella]?.[orphanId]) {
delete userTree[cartella][orphanId];
} }
deletedCount++; // ---------------------------------------------------------
// 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}`);
} }
log(`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`); // ---------------------------------------------------------
// ORFANI
totalNew += newCount; // ---------------------------------------------------------
totalDeleted += deletedCount; for (const orphanId of idsSet) {
totalUnchanged += unchangedCount; log(` 🔴 Cancellato ${orphanId}`);
await deleteThumbsById(orphanId);
await deleteFromDB(orphanId, user);
}
} }
// --------------------------------------------------------- // ---------------------------------------------------------
// SCAN SOTTOCARTELLE // SCAN DI TUTTE LE CARTELLE
// --------------------------------------------------------- // ---------------------------------------------------------
let entries = []; let entries = [];
try { try {
@ -211,35 +316,26 @@ async function scanPhoto(dir, userName, db) {
} }
// --------------------------------------------------------- // ---------------------------------------------------------
// SALVO INDEX // INVIO AL SERVER REMOTO
// ---------------------------------------------------------
if (WRITE_INDEX) {
await saveIndex(nextIndexTree);
}
// ---------------------------------------------------------
// INVIO AL SERVER / POPOLAZIONE DB
// --------------------------------------------------------- // ---------------------------------------------------------
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) { if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
log(`📤 [SEND START] newFiles=${newFiles.length}`);
for (const p of newFiles) { for (const p of newFiles) {
const fileName = p.path.split('/').pop();
try { try {
await postWithAuth(`${BASE_URL}/photos`, p); await postWithAuth(`${BASE_URL}/photos`, p);
log(`📤 Inviato al server: ${fileName}`); log(`📥 [SENT TO SERVER] ${p.name}`);
} catch (err) { } catch (err) {
log(`Errore invio ${fileName}: ${err.message}`); log(`❌ [SERVER SENT ERROR] ${p.name} ${err.message}`);
} }
} }
} else {
log(`⚠️ [NO SEND] newFiles=${newFiles.length}`);
} }
// ---------------------------------------------------------
// FINE SCAN
// ---------------------------------------------------------
const elapsed = ((Date.now() - start) / 1000).toFixed(2); const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO in ${elapsed}s`);
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
log(`⏱ Tempo totale: ${elapsed}s`);
return newFiles; return newFiles;
} }

View file

@ -0,0 +1,349 @@
// 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

@ -1,165 +0,0 @@
// scanner/scanPhoto.js
const path = require('path');
const fsp = require('fs/promises');
const { loadPreviousIndex, saveIndex } = require('./indexStore');
const scanCartella = require('./scanCartella');
const postWithAuth = require('./postWithAuth');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
WRITE_INDEX
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
async function scanPhoto(dir, userName, db) {
const start = Date.now();
console.log(`\n🔵 Inizio scan globale per user=${userName}`);
const previousIndexTree = await loadPreviousIndex();
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
const {
buildIdsListForFolder,
removeIdFromList,
deleteThumbsById,
deleteFromDB
} = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let totalNew = 0;
let totalDeleted = 0;
let totalUnchanged = 0;
// 🔥 Array dei file nuovi/modificati (per DB e server)
let newFiles = [];
// ---------------------------------------------------------
// SCAN DI UNA SINGOLA CARTELLA
// ---------------------------------------------------------
async function scanSingleFolder(user, cartella, absCartella) {
console.log(`\n📁 Inizio cartella: ${cartella}`);
let idsIndex = await buildIdsListForFolder(user, cartella);
let newCount = 0;
let unchangedCount = 0;
let deletedCount = 0;
// STREAMING DEI FILE
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
const fileName = m.path.split('/').pop();
// ⚪ FILE INVARIATO
if (m.unchanged) {
console.log(` ⚪ Invariato: ${fileName}`);
idsIndex = removeIdFromList(idsIndex, m.id);
unchangedCount++;
continue;
}
// 🟢 FILE NUOVO O MODIFICATO
console.log(` 🟢 Nuovo/Modificato: ${fileName}`);
nextIndexTree[m.user] ??= {};
nextIndexTree[m.user][m.cartella] ??= {};
nextIndexTree[m.user][m.cartella][m.id] = {
id: m.id,
user: m.user,
cartella: m.cartella,
path: m.path,
hash: m._indexHash
};
idsIndex = removeIdFromList(idsIndex, m.id);
newCount++;
// Aggiungiamo alla lista dei file nuovi
newFiles.push(m);
}
// 🔴 ORFANI (FILE CANCELLATI)
for (const orphanId of idsIndex) {
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
const fileName = old?.path?.split('/').pop() || orphanId;
console.log(` 🔴 Eliminato: ${fileName}`);
await deleteThumbsById(orphanId);
deleteFromDB(orphanId);
const userTree = nextIndexTree[user];
if (userTree?.[cartella]?.[orphanId]) {
delete userTree[cartella][orphanId];
}
deletedCount++;
}
console.log(
`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`
);
totalNew += newCount;
totalDeleted += deletedCount;
totalUnchanged += unchangedCount;
}
// ---------------------------------------------------------
// SCAN SOTTOCARTELLE
// ---------------------------------------------------------
let entries = [];
try {
entries = await fsp.readdir(userDir, { withFileTypes: true });
} catch {
console.log(`Nessuna directory per utente ${userName}`);
return [];
}
for (const e of entries) {
if (!e.isDirectory()) continue;
const cartella = e.name;
const absCartella = path.join(userDir, cartella);
await scanSingleFolder(userName, cartella, absCartella);
}
// ---------------------------------------------------------
// SALVO INDEX
// ---------------------------------------------------------
if (WRITE_INDEX) {
await saveIndex(nextIndexTree);
}
// ---------------------------------------------------------
// INVIO AL SERVER / POPOLAZIONE DB
// ---------------------------------------------------------
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
for (const p of newFiles) {
const fileName = p.path.split('/').pop();
try {
await postWithAuth(`${BASE_URL}/photos`, p);
console.log(`📤 Inviato al server: ${fileName}`);
} catch (err) {
console.error(`Errore invio ${fileName}:`, err.message);
}
}
}
// ---------------------------------------------------------
// FINE SCAN
// ---------------------------------------------------------
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
console.log(`\n🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
console.log(`⏱ Tempo totale: ${elapsed}s\n`);
return newFiles;
}
module.exports = scanPhoto;

View file

@ -1,142 +0,0 @@
// scanner/scanPhoto.js
const path = require('path');
const fsp = require('fs/promises');
const { log } = require('./logger');
const { loadPreviousIndex, saveIndex } = require('./indexStore');
const scanCartella = require('./scanCartella');
const postWithAuth = require('./postWithAuth');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
WRITE_INDEX
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
async function scanPhoto(dir, userName, db) {
const start = Date.now();
log(`🔵 Inizio scan globale per user=${userName}`);
const previousIndexTree = await loadPreviousIndex();
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
const {
buildIdsListForFolder,
removeIdFromList,
deleteThumbsById,
deleteFromDB
} = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let totalNew = 0;
let totalDeleted = 0;
let totalUnchanged = 0;
let newFiles = [];
async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`);
let idsIndex = await buildIdsListForFolder(user, cartella);
let newCount = 0;
let unchangedCount = 0;
let deletedCount = 0;
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
const fileName = m.path.split('/').pop();
if (m.unchanged) {
log(` ⚪ Invariato: ${fileName}`);
idsIndex = removeIdFromList(idsIndex, m.id);
unchangedCount++;
continue;
}
log(` 🟢 Nuovo/Modificato: ${fileName}`);
nextIndexTree[m.user] ??= {};
nextIndexTree[m.user][m.cartella] ??= {};
nextIndexTree[m.user][m.cartella][m.id] = {
id: m.id,
user: m.user,
cartella: m.cartella,
path: m.path,
hash: m._indexHash
};
idsIndex = removeIdFromList(idsIndex, m.id);
newCount++;
newFiles.push(m);
}
for (const orphanId of idsIndex) {
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
const fileName = old?.path?.split('/').pop() || orphanId;
log(` 🔴 Eliminato: ${fileName}`);
await deleteThumbsById(orphanId);
deleteFromDB(orphanId);
const userTree = nextIndexTree[user];
if (userTree?.[cartella]?.[orphanId]) {
delete userTree[cartella][orphanId];
}
deletedCount++;
}
log(`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`);
totalNew += newCount;
totalDeleted += deletedCount;
totalUnchanged += unchangedCount;
}
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);
}
if (WRITE_INDEX) {
await saveIndex(nextIndexTree);
}
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
for (const p of newFiles) {
const fileName = p.path.split('/').pop();
try {
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📤 Inviato al server: ${fileName}`);
} catch (err) {
log(`Errore invio ${fileName}: ${err.message}`);
}
}
}
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
log(`⏱ Tempo totale: ${elapsed}s`);
return newFiles;
}
module.exports = scanPhoto;

View file

@ -1,144 +0,0 @@
// scanner/scanPhoto.js
const path = require('path');
const fsp = require('fs/promises');
const { log } = require('./logger');
const { loadPreviousIndex, saveIndex } = require('./indexStore');
const scanCartella = require('./scanCartella');
const postWithAuth = require('./postWithAuth');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
WRITE_INDEX
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
async function scanPhoto(dir, userName, db) {
const start = Date.now();
log(`🔵 Inizio scan globale per user=${userName}`);
const previousIndexTree = await loadPreviousIndex();
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
const {
buildIdsListForFolder,
removeIdFromList,
deleteThumbsById,
deleteFromDB
} = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let totalNew = 0;
let totalDeleted = 0;
let totalUnchanged = 0;
let newFiles = [];
async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`);
let idsIndex = await buildIdsListForFolder(user, cartella);
let newCount = 0;
let unchangedCount = 0;
let deletedCount = 0;
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
const fileName = m.path.split('/').pop();
// ⚪ FILE INVARIATO → NON LOGGARE
if (m.unchanged) {
idsIndex = removeIdFromList(idsIndex, m.id);
unchangedCount++;
continue;
}
// 🟢 FILE NUOVO O MODIFICATO
log(` 🟢 Nuovo/Modificato: ${fileName}`);
nextIndexTree[m.user] ??= {};
nextIndexTree[m.user][m.cartella] ??= {};
nextIndexTree[m.user][m.cartella][m.id] = {
id: m.id,
user: m.user,
cartella: m.cartella,
path: m.path,
hash: m._indexHash
};
idsIndex = removeIdFromList(idsIndex, m.id);
newCount++;
newFiles.push(m);
}
// 🔴 ORFANI
for (const orphanId of idsIndex) {
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
const fileName = old?.path?.split('/').pop() || orphanId;
log(` 🔴 Eliminato: ${fileName}`);
await deleteThumbsById(orphanId);
deleteFromDB(orphanId);
const userTree = nextIndexTree[user];
if (userTree?.[cartella]?.[orphanId]) {
delete userTree[cartella][orphanId];
}
deletedCount++;
}
log(`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`);
totalNew += newCount;
totalDeleted += deletedCount;
totalUnchanged += unchangedCount;
}
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);
}
if (WRITE_INDEX) {
await saveIndex(nextIndexTree);
}
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
for (const p of newFiles) {
const fileName = p.path.split('/').pop();
try {
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📤 Inviato al server: ${fileName}`);
} catch (err) {
log(`Errore invio ${fileName}: ${err.message}`);
}
}
}
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
log(`⏱ Tempo totale: ${elapsed}s`);
return newFiles;
}
module.exports = scanPhoto;

View file

@ -1,165 +0,0 @@
// scanner/scanPhoto.js
const path = require('path');
const fsp = require('fs/promises');
const { log } = require('./logger');
const { loadPreviousIndex, saveIndex } = require('./indexStore');
const scanCartella = require('./scanCartella');
const postWithAuth = require('./postWithAuth');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
WRITE_INDEX
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
// Variabile per log completo o ridotto
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
async function scanPhoto(dir, userName, db) {
const start = Date.now();
log(`🔵 Inizio scan globale per user=${userName}`);
const previousIndexTree = await loadPreviousIndex();
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
const {
buildIdsListForFolder,
removeIdFromList,
deleteThumbsById,
deleteFromDB
} = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let totalNew = 0;
let totalDeleted = 0;
let totalUnchanged = 0;
let newFiles = [];
// ---------------------------------------------------------
// SCAN DI UNA SINGOLA CARTELLA
// ---------------------------------------------------------
async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`);
let idsIndex = await buildIdsListForFolder(user, cartella);
let newCount = 0;
let unchangedCount = 0;
let deletedCount = 0;
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
const fileName = m.path.split('/').pop();
// ⚪ FILE INVARIATO
if (m.unchanged) {
if (LOG_VERBOSE) {
log(` ⚪ Invariato: ${fileName}`);
}
idsIndex = removeIdFromList(idsIndex, m.id);
unchangedCount++;
continue;
}
// 🟢 FILE NUOVO O MODIFICATO
log(` 🟢 Nuovo/Modificato: ${fileName}`);
nextIndexTree[m.user] ??= {};
nextIndexTree[m.user][m.cartella] ??= {};
nextIndexTree[m.user][m.cartella][m.id] = {
id: m.id,
user: m.user,
cartella: m.cartella,
path: m.path,
hash: m._indexHash
};
idsIndex = removeIdFromList(idsIndex, m.id);
newCount++;
newFiles.push(m);
}
// 🔴 ORFANI (FILE CANCELLATI)
for (const orphanId of idsIndex) {
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
const fileName = old?.path?.split('/').pop() || orphanId;
log(` 🔴 Eliminato: ${fileName}`);
await deleteThumbsById(orphanId);
deleteFromDB(orphanId);
const userTree = nextIndexTree[user];
if (userTree?.[cartella]?.[orphanId]) {
delete userTree[cartella][orphanId];
}
deletedCount++;
}
log(`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`);
totalNew += newCount;
totalDeleted += deletedCount;
totalUnchanged += unchangedCount;
}
// ---------------------------------------------------------
// SCAN SOTTOCARTELLE
// ---------------------------------------------------------
let entries = [];
try {
entries = await fsp.readdir(userDir, { withFileTypes: true });
} catch {
log(`Nessuna directory per utente ${userName}`);
return [];
}
for (const e of entries) {
if (!e.isDirectory()) continue;
const cartella = e.name;
const absCartella = path.join(userDir, cartella);
await scanSingleFolder(userName, cartella, absCartella);
}
// ---------------------------------------------------------
// SALVO INDEX
// ---------------------------------------------------------
if (WRITE_INDEX) {
await saveIndex(nextIndexTree);
}
// ---------------------------------------------------------
// INVIO AL SERVER / POPOLAZIONE DB
// ---------------------------------------------------------
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
for (const p of newFiles) {
const fileName = p.path.split('/').pop();
try {
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📤 Inviato al server: ${fileName}`);
} catch (err) {
log(`Errore invio ${fileName}: ${err.message}`);
}
}
}
// ---------------------------------------------------------
// FINE SCAN
// ---------------------------------------------------------
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
log(`⏱ Tempo totale: ${elapsed}s`);
return newFiles;
}
module.exports = scanPhoto;

View file

@ -1,192 +0,0 @@
// scanner/scanPhoto.js
const path = require('path');
const fsp = require('fs/promises');
const { log } = require('./logger');
const { loadPreviousIndex, saveIndex } = require('./indexStore');
const scanCartella = require('./scanCartella');
const postWithAuth = require('./postWithAuth');
const {
WEB_ROOT,
SEND_PHOTOS,
BASE_URL,
WRITE_INDEX
} = require('../config');
const createCleanupFunctions = require('./orphanCleanup');
// Variabile per log completo o ridotto
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
async function scanPhoto(dir, userName, db) {
const start = Date.now();
log(`🔵 Inizio scan globale per user=${userName}`);
const previousIndexTree = await loadPreviousIndex();
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
const {
buildIdsListForFolder,
removeIdFromList,
deleteThumbsById,
deleteFromDB
} = createCleanupFunctions(db);
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
const userDir = path.join(photosRoot, userName, 'original');
let totalNew = 0;
let totalDeleted = 0;
let totalUnchanged = 0;
let newFiles = [];
// ---------------------------------------------------------
// 1) CALCOLO TOTALE FILE (contatore globale)
// ---------------------------------------------------------
let TOTAL_FILES = 0;
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);
for await (const _ of scanCartella(userName, cartella, absCartella, previousIndexTree)) {
TOTAL_FILES++;
}
}
if (TOTAL_FILES === 0) {
log(`Nessun file trovato per user=${userName}`);
return [];
}
log(`📦 File totali da processare: ${TOTAL_FILES}`);
// contatore progressivo
let CURRENT = 0;
// ---------------------------------------------------------
// 2) SCAN REALE DELLE CARTELLE
// ---------------------------------------------------------
async function scanSingleFolder(user, cartella, absCartella) {
log(`📁 Inizio cartella: ${cartella}`);
let idsIndex = await buildIdsListForFolder(user, cartella);
let newCount = 0;
let unchangedCount = 0;
let deletedCount = 0;
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
CURRENT++;
const fileName = m.path.split('/').pop();
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
// ⚪ FILE INVARIATO
if (m.unchanged) {
if (LOG_VERBOSE) {
log(`${prefix} ⚪ Invariato: ${fileName}`);
}
idsIndex = removeIdFromList(idsIndex, m.id);
unchangedCount++;
continue;
}
// 🟢 FILE NUOVO O MODIFICATO
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
nextIndexTree[m.user] ??= {};
nextIndexTree[m.user][m.cartella] ??= {};
nextIndexTree[m.user][m.cartella][m.id] = {
id: m.id,
user: m.user,
cartella: m.cartella,
path: m.path,
hash: m._indexHash
};
idsIndex = removeIdFromList(idsIndex, m.id);
newCount++;
newFiles.push(m);
}
// 🔴 ORFANI
for (const orphanId of idsIndex) {
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
const fileName = old?.path?.split('/').pop() || orphanId;
log(` 🔴 Eliminato: ${fileName}`);
await deleteThumbsById(orphanId);
deleteFromDB(orphanId);
const userTree = nextIndexTree[user];
if (userTree?.[cartella]?.[orphanId]) {
delete userTree[cartella][orphanId];
}
deletedCount++;
}
log(`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`);
totalNew += newCount;
totalDeleted += deletedCount;
totalUnchanged += unchangedCount;
}
// ---------------------------------------------------------
// SCAN SOTTOCARTELLE (REPLAY)
// ---------------------------------------------------------
for (const e of entries) {
if (!e.isDirectory()) continue;
const cartella = e.name;
const absCartella = path.join(userDir, cartella);
await scanSingleFolder(userName, cartella, absCartella);
}
// ---------------------------------------------------------
// SALVO INDEX
// ---------------------------------------------------------
if (WRITE_INDEX) {
await saveIndex(nextIndexTree);
}
// ---------------------------------------------------------
// INVIO AL SERVER / POPOLAZIONE DB
// ---------------------------------------------------------
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
for (const p of newFiles) {
const fileName = p.path.split('/').pop();
try {
await postWithAuth(`${BASE_URL}/photos`, p);
log(`📤 Inviato al server: ${fileName}`);
} catch (err) {
log(`Errore invio ${fileName}: ${err.message}`);
}
}
}
// ---------------------------------------------------------
// FINE SCAN
// ---------------------------------------------------------
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
log(`⏱ Tempo totale: ${elapsed}s`);
return newFiles;
}
module.exports = scanPhoto;

View file

@ -1,647 +0,0 @@
// server.js
require('dotenv').config();
const fs = require('fs');
const fsp = require('fs/promises');
const path = require('path');
const jsonServer = require('json-server');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const scanPhoto = require('./scanner/scanPhoto.js');
const { WEB_ROOT, INDEX_PATH } = require('./config');
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
const PORT = process.env.SERVER_PORT || 4000;
const server = jsonServer.create();
// -----------------------------------------------------
// STATIC FILES
// -----------------------------------------------------
server.use(
jsonServer.defaults({
static: path.join(__dirname, '../public'),
})
);
// -----------------------------------------------------
// CONFIG ENDPOINT (PUBBLICO)
// -----------------------------------------------------
server.get('/config', (req, res) => {
res.json({
baseUrl: process.env.BASE_URL,
pathFull: process.env.PATH_FULL,
});
});
// -----------------------------------------------------
// ROUTER DB
// -----------------------------------------------------
let router;
if (fs.existsSync('./api_v1/db.json')) {
router = jsonServer.router('./api_v1/db.json');
} else {
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
fs.writeFileSync('api_v1/db.json', initialData);
router = jsonServer.router('./api_v1/db.json');
}
// -----------------------------------------------------
// USERS DB
// -----------------------------------------------------
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8'));
server.use(jsonServer.bodyParser);
// -----------------------------------------------------
// JWT HELPERS
// -----------------------------------------------------
function createToken(payload) {
return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN });
}
const denylist = new Map();
function addToDenylist(token) {
try {
const decoded = jwt.decode(token);
const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60;
denylist.set(token, exp);
} catch {
denylist.set(token, Math.floor(Date.now() / 1000) + 60);
}
}
function isRevoked(token) {
const exp = denylist.get(token);
if (!exp) return false;
if (exp * 1000 < Date.now()) {
denylist.delete(token);
return false;
}
return true;
}
function verifyToken(token) {
return jwt.verify(token, SECRET_KEY);
}
function isAuthenticated({ email, password }) {
return (
userdb.users.findIndex(
(user) => user.email === email && bcrypt.compareSync(password, user.password)
) !== -1
);
}
// -----------------------------------------------------
// RESET DB (utility interna usata da /initDB)
// -----------------------------------------------------
function resetDB() {
const initialData = fs.readFileSync('api_v1/initialDB.json', 'utf8');
fs.writeFileSync('api_v1/db.json', initialData);
router.db.setState(JSON.parse(initialData));
console.log('DB resettato');
}
// -----------------------------------------------------
// Rimuove tutte le directories thumbs se user = Admin altrimenti solo quella dello user (Public/photos/<user>/thumbs)
// -----------------------------------------------------
async function removeAllThumbs(user) {
const photosRoot = path.resolve(__dirname, '../public/photos');
if (!fs.existsSync(photosRoot)) {
console.log('Nessuna cartella photos trovata, niente thumbs da cancellare');
return;
}
// Se NON è Admin → cancella solo la sua cartella
if (user !== 'Admin') {
const thumbsDir = path.join(photosRoot, user, 'thumbs');
try {
await fsp.rm(thumbsDir, { recursive: true, force: true });
console.log(`✔ thumbs rimosse per utente "${user}": ${thumbsDir}`);
} catch (err) {
console.error(`✖ errore rimuovendo thumbs per "${user}":`, err);
}
console.log(`🎉 Cancellazione thumbs completata per utente "${user}"`);
return;
}
// Se è Admin → cancella TUTTE le thumbs
const entries = await fsp.readdir(photosRoot, { withFileTypes: true });
let removed = 0;
let total = 0;
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const userDir = path.join(photosRoot, entry.name);
const thumbsDir = path.join(userDir, 'thumbs');
total++;
try {
await fsp.rm(thumbsDir, { recursive: true, force: true });
removed++;
console.log(`✔ thumbs rimossa: ${thumbsDir}`);
} catch (err) {
console.error(`✖ errore rimuovendo ${thumbsDir}:`, err);
}
}
// LOG FINALE ADMIN
if (removed === total) {
console.log(`🎉 Tutte le cartelle thumbs (${removed}) sono state cancellate`);
} else {
console.log(`⚠ Cancellate ${removed} cartelle thumbs su ${total}`);
}
}
// -----------------------------------------------------
// Cancella una foto da index.json usando l'id
// -----------------------------------------------------
async function deleteFromIndexById(id) {
const indexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH);
if (!fs.existsSync(indexPath)) {
console.log("index.json non trovato");
return false;
}
const raw = await fsp.readFile(indexPath, 'utf8');
const index = JSON.parse(raw);
let deleted = false;
for (const user of Object.keys(index)) {
const userObj = index[user];
if (!userObj || typeof userObj !== 'object') continue;
for (const cartella of Object.keys(userObj)) {
const folder = userObj[cartella];
if (!folder || typeof folder !== 'object') continue;
if (folder[id]) {
delete folder[id];
deleted = true;
console.log(`✔ Eliminato ID ${id} da ${user}/${cartella}`);
}
}
}
if (deleted) {
await fsp.writeFile(indexPath, JSON.stringify(index, null, 2));
}
return deleted;
}
// -----------------------------------------------------
// Cancella i thumbs usando l'id
// -----------------------------------------------------
async function deleteThumbsById(id) {
console.log(`\n=== DELETE THUMBS FOR ID: ${id} ===`);
const db = router.db;
const col = db.get('photos');
const rec = col.find({ id }).value();
if (!rec) {
console.log("Record non trovato nel DB → impossibile cancellare thumbs");
return false;
}
const thumb1 = rec.thub1;
const thumb2 = rec.thub2;
if (!thumb1 && !thumb2) {
console.log("Nessun thumb registrato nel DB");
return false;
}
// Costruzione corretta del percorso assoluto
const absThumb1 = thumb1 ? path.resolve(__dirname, '../public' + thumb1) : null;
const absThumb2 = thumb2 ? path.resolve(__dirname, '../public' + thumb2) : null;
console.log(`Thumb1 path: ${absThumb1}`);
console.log(`Thumb2 path: ${absThumb2}`);
let deleted = false;
if (absThumb1) {
const exists1 = fs.existsSync(absThumb1);
console.log(`Thumb1 exists: ${exists1}`);
if (exists1) {
await fsp.rm(absThumb1, { force: true });
console.log("✔ Eliminato thumb1");
deleted = true;
} else {
console.log("✖ thumb1 NON trovato");
}
}
if (absThumb2) {
const exists2 = fs.existsSync(absThumb2);
console.log(`Thumb2 exists: ${exists2}`);
if (exists2) {
await fsp.rm(absThumb2, { force: true });
console.log("✔ Eliminato thumb2");
deleted = true;
} else {
console.log("✖ thumb2 NON trovato");
}
}
console.log(`=== FINE DELETE THUMBS ID: ${id} ===\n`);
return deleted;
}
// -----------------------------------------------------
// Elimina uno user dal DB e lo salva
// -----------------------------------------------------
function deleteUserRecords(username) {
const db = router.db; // lowdb instance
const col = db.get('photos');
// Rimuove i record e salva su disco
const removed = col.remove({ user: username }).write();
console.log(`Eliminati ${removed.length} record per user "${username}"`);
return removed.length;
}
// -----------------------------------------------------
// HOME
// -----------------------------------------------------
server.get('/', (req, res) => {
res.sendFile(path.resolve('public/index.html'));
});
// -----------------------------------------------------
// LOGIN (PUBBLICO)
// -----------------------------------------------------
server.post('/auth/login', (req, res) => {
const { email, password } = req.body;
const user = userdb.users.find((u) => u.email === email);
if (!user || !bcrypt.compareSync(password, user.password)) {
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
}
const token = createToken({
id: user.id,
email: user.email,
name: user.name,
});
res.status(200).json({
token,
name: user.name,
});
});
// -----------------------------------------------------
// LOGOUT
// -----------------------------------------------------
server.post('/auth/logout', (req, res) => {
const auth = req.headers.authorization || '';
const [scheme, token] = auth.split(' ');
if (scheme === 'Bearer' && token) {
addToDenylist(token);
}
return res.status(204).end();
});
// -----------------------------------------------------
// JWT MIDDLEWARE (tutte le rotte tranne /auth/*)
// -----------------------------------------------------
server.use(/^(?!\/auth).*$/, (req, res, next) => {
const auth = req.headers.authorization || '';
const [scheme, token] = auth.split(' ');
if (scheme !== 'Bearer' || !token) {
return res.status(401).json({ status: 401, message: 'Bad authorization header' });
}
if (isRevoked(token)) {
return res.status(401).json({ status: 401, message: 'Token revoked' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (err) {
return res
.status(401)
.json({ status: 401, message: 'Error: access_token is not valid' });
}
});
// -----------------------------------------------------
// FILTRO AUTOMATICO PER USER (GET)
// - Non-Admin: forzo user=[<nome>, 'Common'] così vedono anche la Common
// - Admin: vede tutto senza forzature
// -----------------------------------------------------
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']));
}
next();
});
// -----------------------------------------------------
// SCAN FOTO
// - Admin: scansiona tutti gli utenti + Common
// - Non-Admin: scansiona solo la propria area (NO Common)
// -----------------------------------------------------
server.get('/scanold', async (req, res) => {
try {
if (req.user && req.user.name === 'Admin') {
await scanPhoto(undefined, 'Admin');
return res.send({
status: 'Scansione completata',
user: 'Admin',
scope: 'tutti gli utenti + Common',
});
}
// Non-Admin → solo la sua area (niente Common)
await scanPhoto(undefined, req.user.name);
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 });
}
});
server.get('/scan', async (req, res) => {
try {
if (req.user && req.user.name === 'Admin') {
await scanPhoto(undefined, 'Admin', router.db);
return res.send({
status: 'Scansione completata',
user: 'Admin',
scope: 'tutti gli utenti + Common',
});
}
// Non-Admin → solo la sua area (niente Common)
await scanPhoto(undefined, req.user.name, router.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 });
}
});
// -----------------------------------------------------
// FILE STATICI
// -----------------------------------------------------
server.get('/files', (req, res) => {
const requested = req.query.file || '';
const publicDir = path.resolve(path.join(__dirname, '../public'));
const resolved = path.resolve(publicDir, requested);
if (!resolved.startsWith(publicDir)) {
return res.status(400).json({ error: 'Invalid path' });
}
if (!fs.existsSync(resolved) || fs.statSync(resolved).isDirectory()) {
return res.status(404).json({ error: 'Not found' });
}
res.sendFile(resolved);
});
// -----------------------------------------------------
// RESET DB MANUALE + rimozione index.json
// -----------------------------------------------------
server.get('/initDB', async (req, res) => {
try {
resetDB();
// Rimuove index.json
const absIndexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH);
try {
await fsp.unlink(absIndexPath);
console.log('initDB: index.json rimosso ->', absIndexPath);
} catch (err) {
if (err.code === 'ENOENT') {
console.log('initDB: index.json non trovato:', absIndexPath);
} else {
console.error('initDB: errore cancellando index.json:', err);
}
}
// rimuove tutte le cartelle thumbs
await removeAllThumbs('Admin');
res.json({
status: 'DB resettato',
indexRemoved: true,
thumbsRemoved: true,
indexPath: absIndexPath
});
} catch (err) {
console.error('initDB: errore generale:', err);
res.status(500).json({ status: 'errore', message: err.message });
}
});
// -----------------------------------------------------
// DELETE FOTO da DB + index.json + thumbs
// -----------------------------------------------------
server.delete('/delphoto/:id', async (req, res) => {
const { id } = req.params;
try {
const db = router.db;
const col = db.get('photos');
const existing = col.find({ id }).value();
// 1) Cancella thumbs PRIMA DI TUTTO
const deletedThumbs = await deleteThumbsById(id);
let deletedDB = false;
// 2) Cancella dal DB
if (existing) {
col.remove({ id }).write();
deletedDB = true;
console.log(`DELPHOTO → foto cancellata dal DB: ${id}`);
} else {
console.log(`DELPHOTO → foto NON trovata nel DB: ${id}`);
}
// 3) Cancella da index.json
const deletedIndex = await deleteFromIndexById(id);
return res.json({
ok: true,
id,
deletedThumbs,
deletedDB,
deletedIndex
});
} catch (err) {
console.error('DELPHOTO errore:', err);
return res.status(500).json({ ok: false, error: err.message });
}
});
// -----------------------------------------------------
// RESET DB SOLO PER UN UTENTE
// - Admin: deve specificare ?user=<nome>
// - Non-Admin: cancella solo i propri dati
// -----------------------------------------------------
server.get('/initDBuser', async (req, res) => {
try {
let targetUser = req.user.name;
// Admin può specificare chi cancellare
if (req.user.name === 'Admin') {
targetUser = req.query.user;
if (!targetUser) {
return res.status(400).json({
error: "Admin deve specificare ?user=<nome>"
});
}
}
// 1) Cancella record DB
const deleted = deleteUserRecords(targetUser);
// 2) Cancella thumbs
await removeAllThumbs(targetUser);
res.json({
status: "OK",
user: targetUser,
deletedRecords: deleted,
thumbsRemoved: true
});
} catch (err) {
console.error("initDBuser errore:", err);
res.status(500).json({ error: "Errore durante initDBuser", details: err.message });
}
});
// -----------------------------------------------------
// FIND ID IN INDEX.JSON + RETURN RECORD (SOLO LETTURA)
// -----------------------------------------------------
server.get('/findIdIndex/:id', async (req, res) => {
const { id } = req.params;
try {
const indexPath = path.resolve(__dirname, '..', WEB_ROOT, INDEX_PATH);
if (!fs.existsSync(indexPath)) {
return res.json({ ok: false, found: false, message: "index.json non trovato" });
}
const raw = await fsp.readFile(indexPath, 'utf8');
const index = JSON.parse(raw);
for (const user of Object.keys(index)) {
const userObj = index[user];
if (!userObj || typeof userObj !== 'object') continue;
for (const cartella of Object.keys(userObj)) {
const folder = userObj[cartella];
if (!folder || typeof folder !== 'object') continue;
// dentro la cartella: chiavi = id, più _folderHash
if (folder[id]) {
return res.json({
ok: true,
found: true,
user,
cartella,
record: folder[id]
});
}
}
}
return res.json({ ok: true, found: false });
} catch (err) {
console.error("Errore findIdIndex:", err);
return res.status(500).json({ ok: false, error: err.message });
}
});
// -----------------------------------------------------
// UPSERT anti-duplicato per /photos (prima del router)
// Se id esiste -> aggiorna; altrimenti crea
// -----------------------------------------------------
server.post('/photos', (req, res, next) => {
try {
const id = req.body && req.body.id;
if (!id) return next();
const db = router.db; // lowdb instance
const col = db.get('photos');
const existing = col.find({ id }).value();
if (existing) {
col.find({ id }).assign(req.body).write();
return res.status(200).json(req.body);
}
return next(); // non esiste: crea con il router
} catch (e) {
console.error('UPSERT /photos error:', e);
return res.status(500).json({ error: 'upsert failed' });
}
});
// -----------------------------------------------------
// ROUTER JSON-SERVER
// -----------------------------------------------------
server.use(router);
// -----------------------------------------------------
// START SERVER
// -----------------------------------------------------
server.listen(PORT, () => {
console.log(`Auth API server running on port ${PORT} ...`);
});
// Pulizia denylist
setInterval(() => {
const nowSec = Math.floor(Date.now() / 1000);
for (const [tok, exp] of denylist.entries()) {
if (exp < nowSec) denylist.delete(tok);
}
}, 60 * 1000);

1
b/a.jpg Normal file
View file

@ -0,0 +1 @@
prova

86
db/init.js Normal file
View file

@ -0,0 +1,86 @@
// db/init.js
const db = require('./knex');
async function init() {
// -------------------------------
// 1) Tabella PHOTOS
// -------------------------------
const existsPhotos = await db.schema.hasTable('photos');
if (!existsPhotos) {
await db.schema.createTable('photos', (t) => {
t.string('id').primary();
t.string('user');
t.string('cartella');
t.string('name');
t.string('path');
t.string('thub1');
t.string('thub2');
t.string('mime_type');
t.integer('width');
t.integer('height');
t.integer('rotation');
t.integer('size_bytes');
t.integer('mtimeMs');
t.integer('duration_ms');
t.string('taken_at');
t.string('data');
t.float('lat');
t.float('lon');
t.float('alt');
t.text('location'); // JSON string
// Hash lento (metadati)
t.string('_indexHash');
// Hash veloce (size-mtime)
t.string('fast_hash');
});
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();
}
init();

42
db/init.js.old Normal file
View file

@ -0,0 +1,42 @@
// db/init.js
const db = require('./knex');
async function init() {
const exists = await db.schema.hasTable('photos');
if (!exists) {
await db.schema.createTable('photos', (t) => {
t.string('id').primary();
t.string('user');
t.string('cartella');
t.string('name');
t.string('path');
t.string('thub1');
t.string('thub2');
t.string('mime_type');
t.integer('width');
t.integer('height');
t.integer('rotation');
t.integer('size_bytes');
t.integer('mtimeMs');
t.integer('duration_ms');
t.string('taken_at');
t.string('data');
t.float('lat');
t.float('lon');
t.float('alt');
t.text('location'); // JSON string
// 👉 NUOVO: hash identico al vecchio index.json
t.string('_indexHash');
});
console.log("✔ Tabella 'photos' creata correttamente (con _indexHash)");
} else {
console.log("✔ Tabella 'photos' già esistente");
}
process.exit();
}
init();

12
db/knex.js Normal file
View file

@ -0,0 +1,12 @@
// db/knex.js
const knex = require('knex');
const db = knex({
client: 'better-sqlite3',
connection: {
filename: './api_v1/database.sqlite'
},
useNullAsDefault: true
});
module.exports = db;

10
f.sh Normal file
View file

@ -0,0 +1,10 @@
#!/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 Normal file
View file

@ -0,0 +1,27 @@
#!/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 Normal file
View file

@ -0,0 +1,53 @@
#!/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

1816
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,14 @@
{ {
"name": "gallery-jwt-json-server",
"version": "0.0.1",
"description": "Gallery and JWT Protected REST API with json-server",
"main": "index.js",
"scripts": {
"start-no-auth": "json-server --watch ./api_v1/db.json -s ./public --host 0.0.0.0 --port 4000",
"start": "node ./api_v1/server.js -s ./public",
"hash": "node ./api_v1/tools.js"
},
"keywords": [
"api"
],
"author": "Fabio",
"license": "MIT",
"dependencies": { "dependencies": {
"async": "^3.2.6", "axios": "^1.13.6",
"axios": "^1.13.5",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"body-parser": "^2.2.2", "better-sqlite3": "^12.8.0",
"dotenv": "^17.3.1", "dotenv": "^17.3.1",
"exifreader": "^4.36.2", "exifreader": "^4.37.0",
"json-server": "^0.17.4", "express": "^5.2.1",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"path": "^0.12.7", "knex": "^3.1.0",
"sharp": "^0.34.5" "sharp": "^0.34.5",
"ws": "^8.19.0"
} }
} }

View file

@ -26,6 +26,15 @@
margin-top: 10px; margin-top: 10px;
font-size: 16px; font-size: 16px;
} }
#changesBox {
margin-top: 20px;
padding: 10px;
border: 1px solid #aaa;
border-radius: 5px;
background: #f7f7f7;
width: 350px;
}
</style> </style>
</head> </head>
@ -40,8 +49,19 @@
<button onclick="deletePhoto()">Cancella Foto per ID</button> <button onclick="deletePhoto()">Cancella Foto per ID</button>
<button onclick="findIdIndex()">Cerca ID in index.json</button> <button onclick="findIdIndex()">Cerca ID in index.json</button>
<button onclick="resetDBuser()">Reset DB Utente</button> <button onclick="resetDBuser()">Reset DB Utente</button>
<button onclick="searchPhotoById()">Cerca Foto (nuovo /byIds)</button>
<button onclick="window.location.href='index.html'">Torna alla galleria</button> <button onclick="window.location.href='index.html'">Torna alla galleria</button>
<!-- 🔥 NUOVO BLOCCO: /photos/changes -->
<div id="changesBox">
<h4>Controlla /photos/changes</h4>
<label>Since (data/ora):</label><br>
<input type="datetime-local" id="sinceInput" style="width: 100%; margin-top:5px;"><br><br>
<button onclick="showChanges()">Mostra cambiamenti</button>
</div>
<!-- Barra di avanzamento --> <!-- Barra di avanzamento -->
<div id="progressContainer"> <div id="progressContainer">
<div id="progressBar"></div> <div id="progressBar"></div>
@ -76,6 +96,30 @@ if (!token) {
// FUNZIONI ESISTENTI // FUNZIONI ESISTENTI
// ------------------------- // -------------------------
async function searchPhotoById() {
const id = prompt("Inserisci l'ID della foto da cercare:");
if (!id) return;
const payload = parseJwt(token);
const user = payload.name;
const url = `${BASE_URL}/photos/byIds?id=${encodeURIComponent(id)}&user=${encodeURIComponent(user)}`;
console.log("🔍 [searchPhotoById] URL chiamato:", url);
const res = await fetch(url, {
headers: { "Authorization": "Bearer " + token }
});
const out = await res.json();
console.log("🔍 [searchPhotoById] Risposta:", out);
document.getElementById("out").textContent =
JSON.stringify(out, null, 2);
}
async function deletePhoto() { async function deletePhoto() {
const id = prompt("Inserisci l'ID della foto da cancellare:"); const id = prompt("Inserisci l'ID della foto da cancellare:");
if (!id) return; if (!id) return;
@ -153,18 +197,45 @@ async function resetDB() {
} }
// ------------------------- // -------------------------
// NUOVA PARTE: SCAN + BARRA // NUOVA FUNZIONE: /photos/changes
// -------------------------
async function showChanges() {
const sinceInput = document.getElementById("sinceInput").value;
if (!sinceInput) {
alert("Seleziona una data/ora valida.");
return;
}
// Convertiamo datetime-local → ISO
const sinceISO = new Date(sinceInput).toISOString();
const payload = parseJwt(token);
const user = payload.name;
const url = `${BASE_URL}/photos/changes?since=${encodeURIComponent(sinceISO)}&user=${encodeURIComponent(user)}`;
const res = await fetch(url, {
headers: { "Authorization": "Bearer " + token }
});
const out = await res.json();
document.getElementById("out").textContent =
JSON.stringify(out, null, 2);
}
// -------------------------
// SCAN + BARRA
// ------------------------- // -------------------------
let scanInterval = null; let scanInterval = null;
async function scan() { async function scan() {
// Avvia lo scan sul backend
fetch(`${BASE_URL}/scan`, { fetch(`${BASE_URL}/scan`, {
headers: { "Authorization": "Bearer " + token } headers: { "Authorization": "Bearer " + token }
}); });
// Avvia il polling dello stato
startScanStatusPolling(); startScanStatusPolling();
} }
@ -173,23 +244,19 @@ async function startScanStatusPolling() {
scanInterval = setInterval(async () => { scanInterval = setInterval(async () => {
try { try {
// PERCORSO CORRETTO
const res = await fetch(`/photos/scan_status.json?ts=` + Date.now()); const res = await fetch(`/photos/scan_status.json?ts=` + Date.now());
if (!res.ok) return; if (!res.ok) return;
const data = await res.json(); const data = await res.json();
// Aggiorna testo
document.getElementById("scanProgress").textContent = document.getElementById("scanProgress").textContent =
`Progresso: ${data.current}/${data.total} (${data.percent}%)`; `Progresso: ${data.current}/${data.total} (${data.percent}%)`;
document.getElementById("scanEta").textContent = document.getElementById("scanEta").textContent =
`Tempo stimato rimanente: ${data.eta}`; `Tempo stimato rimanente: ${data.eta}`;
// Aggiorna barra
document.getElementById("progressBar").style.width = data.percent + "%"; document.getElementById("progressBar").style.width = data.percent + "%";
// Fine scan
if (data.current >= data.total && data.total > 0) { if (data.current >= data.total && data.total > 0) {
clearInterval(scanInterval); clearInterval(scanInterval);
scanInterval = null; scanInterval = null;

View file

@ -1,132 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Photo Manager</title>
</head>
<body>
<div id="app" style="padding:20px;">
<h2>Gestione Foto</h2>
<button onclick="scan()">Scansiona Foto</button>
<button onclick="resetDB()">Reset DB</button>
<button onclick="readDB()">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>
<button onclick="window.location.href='index.html'">Torna alla galleria</button>
<pre id="out"></pre>
</div>
<!-- Eruda Debug Console -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script>
eruda.init();
console.log("Eruda inizializzato");
</script>
<script>
let BASE_URL = null;
let token = localStorage.getItem("token");
let db = [];
// Se non c'è token → torna alla galleria (login avviene lì)
if (!token) {
window.location.href = "index.html";
}
async function deletePhoto() {
const id = prompt("Inserisci l'ID della foto da cancellare:");
if (!id) return;
const res = await fetch(`${BASE_URL}/delphoto/${id}`, {
method: "DELETE",
headers: { "Authorization": "Bearer " + token }
});
const out = await res.json();
document.getElementById("out").textContent =
JSON.stringify(out, null, 2);
await readDB();
}
async function findIdIndex() {
const id = prompt("Inserisci l'ID da cercare in index.json:");
if (!id) return;
const res = await fetch(`${BASE_URL}/findIdIndex/${id}`, {
headers: { "Authorization": "Bearer " + token }
});
const out = await res.json();
document.getElementById("out").textContent =
JSON.stringify(out, null, 2);
}
async function resetDBuser() {
let url = `${BASE_URL}/initDBuser`;
// Se Admin → chiedi quale utente cancellare
const payload = parseJwt(token);
if (payload.name === "Admin") {
const user = prompt("Inserisci il nome dell'utente da cancellare:");
if (!user) return;
url += `?user=${encodeURIComponent(user)}`;
}
await fetch(url, {
headers: { "Authorization": "Bearer " + token }
});
await readDB();
}
// Utility per leggere il token JWT
function parseJwt(t) {
try {
return JSON.parse(atob(t.split('.')[1]));
} catch {
return {};
}
}
async function loadConfig() {
const res = await fetch('/config');
const cfg = await res.json();
BASE_URL = cfg.baseUrl;
}
async function readDB() {
const res = await fetch(`${BASE_URL}/photos`, {
headers: { "Authorization": "Bearer " + token }
});
db = await res.json();
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
}
async function scan() {
await fetch(`${BASE_URL}/scan`, {
headers: { "Authorization": "Bearer " + token }
});
await readDB();
}
async function resetDB() {
await fetch(`${BASE_URL}/initDB`, {
headers: { "Authorization": "Bearer " + token }
});
await readDB();
}
window.onload = async () => {
await loadConfig();
};
</script>
</body>
</html>

View file

@ -45,7 +45,6 @@
title="Logout" title="Logout"
aria-label="Logout"> aria-label="Logout">
<!-- Icona PNG -->
<img <img
class="logout-icon" class="logout-icon"
src="img/switch.png" src="img/switch.png"
@ -70,7 +69,6 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-close" id="modalClose">×</div> <div class="modal-close" id="modalClose">×</div>
<!-- Frecce navigazione -->
<button class="modal-nav-btn prev" id="modalPrev" type="button" aria-label="Precedente">&lt;</button> <button class="modal-nav-btn prev" id="modalPrev" type="button" aria-label="Precedente">&lt;</button>
<button class="modal-nav-btn next" id="modalNext" type="button" aria-label="Successiva">&gt;</button> <button class="modal-nav-btn next" id="modalNext" type="button" aria-label="Successiva">&gt;</button>
@ -143,13 +141,12 @@
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script> <script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
<!-- MarkerCluster JS --> <!-- MarkerCluster JS -->
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script> <script src="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.js"></script>
<!-- Eruda Debug Console --> <!-- Eruda Debug Console -->
<script src="https://cdn.jsdelivr.net/npm/eruda"></script> <script src="https://cdn.jsdelivr.net/npm/eruda"></script>
<script> <script>
eruda.init(); eruda.init();
console.log("Eruda inizializzato");
</script> </script>
<!-- Debug immediato --> <!-- Debug immediato -->
@ -163,12 +160,16 @@
<script src="js/gallery.js"></script> <script src="js/gallery.js"></script>
<script src="js/modal.js"></script> <script src="js/modal.js"></script>
<script src="js/infoPanel.js"></script> <script src="js/infoPanel.js"></script>
<!-- DEVE ESSERE PRIMA DI mapGlobal.js -->
<script src="js/bottomSheet.js"></script> <script src="js/bottomSheet.js"></script>
<script src="js/mapGlobal.js"></script> <script src="js/mapGlobal.js"></script>
<script src="js/logout.js"></script> <script src="js/logout.js"></script>
<!-- 🔥 NUOVI FILE PER SYNC INCREMENTALE -->
<script src="js/api.js"></script>
<script src="js/state.js"></script>
<script src="js/sync.js"></script>
<!-- MAIN -->
<script src="js/main.js"></script> <script src="js/main.js"></script>
</body> </body>

27
public/js/api.js Normal file
View file

@ -0,0 +1,27 @@
// js/api.js
async function apiGet(url) {
return fetch(url, {
headers: { "Authorization": "Bearer " + localStorage.getItem("token") }
}).then(r => r.json());
}
async function getAllPhotos() {
return apiGet(`${BASE_URL}/photos`);
}
async function getPhotoById(id) {
const payload = parseJwt(localStorage.getItem("token"));
const user = payload.name || "Common";
const url = `${BASE_URL}/photos/byIds?id=${encodeURIComponent(id)}&user=${encodeURIComponent(user)}`;
console.log("🔍 [getPhotoById] URL:", url);
return apiGet(url);
}
async function getChanges(since, user) {
return apiGet(`${BASE_URL}/photos/changes?since=${encodeURIComponent(since)}&user=${encodeURIComponent(user)}`);
}

View file

@ -1,13 +1,13 @@
// =============================== // ===============================
// GALLERY — completa, stile Google Photos // GALLERY — con SYNC INCREMENTALE + REFRESH DA .ENV
// - Ordinamento
// - Filtri
// - Raggruppamento (auto/giorno/mese/anno)
// - Render a sezioni
// - Click: openModalFromList(sezione, indice) se disponibile (fallback openModal)
// =============================== // ===============================
// ORDINAMENTO let currentUser = null;
let refreshSeconds = 30;
// ===============================
// 3. ORDINAMENTO
// ===============================
function sortByDate(photos, direction = "desc") { function sortByDate(photos, direction = "desc") {
return photos.slice().sort((a, b) => { return photos.slice().sort((a, b) => {
const da = a?.taken_at ? new Date(a.taken_at) : 0; const da = a?.taken_at ? new Date(a.taken_at) : 0;
@ -16,23 +16,27 @@ function sortByDate(photos, direction = "desc") {
}); });
} }
// FILTRI // ===============================
// 4. FILTRI
// ===============================
function applyFilters(photos) { function applyFilters(photos) {
if (!window.currentFilter) return photos; if (!window.currentFilter) return photos;
switch (window.currentFilter) { switch (window.currentFilter) {
case "folder": case "folder":
return photos.filter(p => p.folder || (p.path && p.path.includes('/photos/'))); return photos.filter(p => p.cartella);
case "location": case "location":
return photos.filter(p => p?.gps && p.gps.lat); return photos.filter(p => p.lat && p.lon);
case "type": case "type":
return photos.filter(p => p?.mime_type && p.mime_type.startsWith("image/")); return photos.filter(p => p?.mime_type?.startsWith("image/"));
default: default:
return photos; return photos;
} }
} }
// RAGGRUPPAMENTO STILE GOOGLE PHOTOS // ===============================
// 5. RAGGRUPPAMENTO
// ===============================
function groupByDate(photos, mode = "auto") { function groupByDate(photos, mode = "auto") {
const sections = []; const sections = [];
const now = new Date(); const now = new Date();
@ -42,6 +46,7 @@ function groupByDate(photos, mode = "auto") {
if (!date || isNaN(+date)) return "Senza data"; if (!date || isNaN(+date)) return "Senza data";
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24)); const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
if (mode === "day") return formatDay(date); if (mode === "day") return formatDay(date);
if (mode === "month") return formatMonth(date); if (mode === "month") return formatMonth(date);
if (mode === "year") return date.getFullYear().toString(); if (mode === "year") return date.getFullYear().toString();
@ -70,7 +75,9 @@ function groupByDate(photos, mode = "auto") {
return sections; return sections;
} }
// FORMATTATORI // ===============================
// 6. FORMATTATORI
// ===============================
function formatDay(date) { function formatDay(date) {
return date.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" }); return date.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" });
} }
@ -78,7 +85,9 @@ function formatMonth(date) {
return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" }); return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" });
} }
// RENDER // ===============================
// 7. RENDER GALLERY
// ===============================
function renderGallery(sections) { function renderGallery(sections) {
const gallery = document.getElementById("gallery"); const gallery = document.getElementById("gallery");
if (!gallery) return; if (!gallery) return;
@ -96,36 +105,11 @@ function renderGallery(sections) {
section.photos.forEach((photo, idx) => { section.photos.forEach((photo, idx) => {
const thumbDiv = document.createElement("div"); const thumbDiv = document.createElement("div");
thumbDiv.className = "thumb"; thumbDiv.className = "thumb";
thumbDiv.id = "photo_" + photo.id;
// const th1 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub1) : photo?.thub1; let th1 = photo?.thub1;
// const th2 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub2 || photo?.thub1) : (photo?.thub2 || photo?.thub1); let th2 = photo?.thub2 || photo?.thub1;
// const original = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.path) : photo?.path; let original = photo?.path;
let th1, th2, original;
if (window.PATH_FULL) {
// Uso direttamente i path completi generati dal backend
th1 = photo?.thub1;
th2 = photo?.thub2 || photo?.thub1;
original = photo?.path;
} else {
// Comportamento attuale: costruisco URL con toAbsoluteUrl()
th1 = (typeof toAbsoluteUrl === 'function')
? toAbsoluteUrl(photo?.thub1, photo?.user, "thumbs", photo?.cartella)
: photo?.thub1;
th2 = (typeof toAbsoluteUrl === 'function')
? toAbsoluteUrl(photo?.thub2 || photo?.thub1, photo?.user, "thumbs", photo?.cartella)
: (photo?.thub2 || photo?.thub1);
original = (typeof toAbsoluteUrl === 'function')
? toAbsoluteUrl(photo?.path, photo?.user, "original", photo?.cartella)
: photo?.path;
}
//console.log(photo?.user);
console.log(th1);
const img = document.createElement("img"); const img = document.createElement("img");
img.src = th1 || th2 || original || ""; img.src = th1 || th2 || original || "";
@ -133,7 +117,7 @@ console.log(th1);
img.loading = "lazy"; img.loading = "lazy";
thumbDiv.appendChild(img); thumbDiv.appendChild(img);
if (photo?.mime_type && photo.mime_type.startsWith("video/")) { if (photo?.mime_type?.startsWith("video/")) {
const play = document.createElement("div"); const play = document.createElement("div");
play.className = "play-icon"; play.className = "play-icon";
play.textContent = "▶"; play.textContent = "▶";
@ -141,7 +125,6 @@ console.log(th1);
} }
thumbDiv.addEventListener("click", () => { thumbDiv.addEventListener("click", () => {
// Chiudi sempre la strip prima di aprire una nuova foto
window.closeBottomSheet?.(); window.closeBottomSheet?.();
if (typeof window.openModalFromList === "function") { if (typeof window.openModalFromList === "function") {
@ -158,8 +141,54 @@ console.log(th1);
}); });
} }
// Esporti su window // ===============================
// 8. REFRESH GALLERY
// ===============================
function refreshGallery() {
const photos = getLocalPhotos(); // 🔥 USO DELLO STATO UNICO
console.log("[refreshGallery] numero foto:", photos.length);
const filtered = applyFilters(photos);
const sorted = sortByDate(filtered, "desc");
const sections = groupByDate(sorted, "auto");
renderGallery(sections);
}
// ===============================
// 9. INIZIALIZZAZIONE GALLERY
// ===============================
async function initGallery() {
console.log("=== INIT GALLERY ===");
console.log("[initGallery] Chiamo /config...");
const cfg = await fetch("/config").then(r => r.json());
console.log("[initGallery] /config RISPOSTA:", cfg);
window.PATH_FULL = cfg.pathFull;
currentUser = cfg?.user || "Common";
refreshSeconds = cfg.galleryRefreshSeconds || 30;
console.log("[initGallery] currentUser =", currentUser);
console.log("[initGallery] refreshSeconds =", refreshSeconds);
console.log("[initGallery] Avvio incrementalSync() iniziale...");
await incrementalSync();
console.log("[initGallery] incrementalSync() iniziale COMPLETATO");
console.log(`[initGallery] Avvio polling ogni ${refreshSeconds} secondi...`);
setInterval(async () => {
console.log(">>> TIMER: chiamata incrementalSync()");
console.log(">>> lastSync attuale =", getLastSync());
await incrementalSync();
console.log(">>> incrementalSync() COMPLETATO (timer)");
}, refreshSeconds * 1000);
}
window.addEventListener("DOMContentLoaded", initGallery);
// EXPORT
window.sortByDate = sortByDate; window.sortByDate = sortByDate;
window.applyFilters = applyFilters; window.applyFilters = applyFilters;
window.groupByDate = groupByDate; window.groupByDate = groupByDate;
window.renderGallery = renderGallery; window.renderGallery = renderGallery;
window.refreshGallery = refreshGallery;

View file

@ -7,30 +7,21 @@ console.log("main.js avviato");
// UTILS AUTH + SYNC HEADER UI // UTILS AUTH + SYNC HEADER UI
// =============================== // ===============================
function isAuthenticated() { function isAuthenticated() {
// Fonte verità: presenza del token in localStorage
return !!localStorage.getItem("token"); return !!localStorage.getItem("token");
} }
/**
* Sincronizza lUI dellheader in base allo stato auth:
* - Aggiunge una classe sul body (CSS-friendly)
* - Mostra/Nasconde il bottone logout (hotfix inline, se vuoi puoi affidarti solo al CSS)
*/
function syncHeaderAuthUI() { function syncHeaderAuthUI() {
const authed = isAuthenticated(); const authed = isAuthenticated();
document.body.classList.toggle('authenticated', authed); document.body.classList.toggle('authenticated', authed);
const logoutBtn = document.getElementById('logoutBtn'); const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) { if (logoutBtn) {
// Hotfix immediato: forza la visibilità anche via inline style
// (puoi rimuovere queste due righe se preferisci usare solo la regola CSS body.authenticated #logoutBtn)
logoutBtn.style.display = authed ? 'inline-flex' : 'none'; logoutBtn.style.display = authed ? 'inline-flex' : 'none';
} }
} }
// =============================== // ===============================
// PATCH: misura l'altezza reale dell'header e aggiorna --header-h // PATCH HEADER HEIGHT
// (serve per far partire la mappa subito sotto lheader, anche su mobile)
// =============================== // ===============================
(function () { (function () {
const root = document.documentElement; const root = document.documentElement;
@ -55,8 +46,7 @@ function syncHeaderAuthUI() {
})(); })();
// =============================== // ===============================
// PATCH: quando si apre la mappa (#globalMap.open) invalida le dimensioni Leaflet // PATCH MAP invalidateSize
// (utile perché prima era display:none; invalidateSize evita “tagli” o tile sfasati)
// =============================== // ===============================
(function () { (function () {
const mapEl = document.getElementById('globalMap'); const mapEl = document.getElementById('globalMap');
@ -64,10 +54,8 @@ function syncHeaderAuthUI() {
function invalidateWhenOpen() { function invalidateWhenOpen() {
if (!mapEl.classList.contains('open')) return; if (!mapEl.classList.contains('open')) return;
// Aspetta un tick così il layout è aggiornato
setTimeout(() => { setTimeout(() => {
try { try {
// In mapGlobal.js imposta: window.leafletMapInstance = window.globalMap;
window.leafletMapInstance?.invalidateSize(); window.leafletMapInstance?.invalidateSize();
} catch (e) { } catch (e) {
console.warn('invalidateSize non eseguito:', e); console.warn('invalidateSize non eseguito:', e);
@ -75,7 +63,6 @@ function syncHeaderAuthUI() {
}, 0); }, 0);
} }
// 1) Osserva il cambio classe (quando aggiungi .open)
const mo = new MutationObserver((mutations) => { const mo = new MutationObserver((mutations) => {
if (mutations.some(m => m.attributeName === 'class')) { if (mutations.some(m => m.attributeName === 'class')) {
invalidateWhenOpen(); invalidateWhenOpen();
@ -83,14 +70,13 @@ function syncHeaderAuthUI() {
}); });
mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] }); mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] });
// 2) Fallback: se usi il bottone #openMapBtn per aprire/chiudere
document.getElementById('openMapBtn')?.addEventListener('click', () => { document.getElementById('openMapBtn')?.addEventListener('click', () => {
setTimeout(invalidateWhenOpen, 0); setTimeout(invalidateWhenOpen, 0);
}); });
})(); })();
// =============================== // ===============================
// MENU ⋮ (toggle apri/chiudi con lo stesso bottone, senza global conflicts) // MENU ⋮
// =============================== // ===============================
(() => { (() => {
const optBtn = document.getElementById("optionsBtn"); const optBtn = document.getElementById("optionsBtn");
@ -103,7 +89,6 @@ function syncHeaderAuthUI() {
try { window.closeBottomSheet?.(); } catch {} try { window.closeBottomSheet?.(); } catch {}
optSheet.classList.add("open"); optSheet.classList.add("open");
overlayEl?.classList.add("open"); overlayEl?.classList.add("open");
// ARIA (facoltativo)
optBtn.setAttribute("aria-expanded", "true"); optBtn.setAttribute("aria-expanded", "true");
optSheet.setAttribute("aria-hidden", "false"); optSheet.setAttribute("aria-hidden", "false");
} }
@ -111,7 +96,6 @@ function syncHeaderAuthUI() {
function closeOptionsSheet() { function closeOptionsSheet() {
optSheet.classList.remove("open"); optSheet.classList.remove("open");
overlayEl?.classList.remove("open"); overlayEl?.classList.remove("open");
// ARIA (facoltativo)
optBtn.setAttribute("aria-expanded", "false"); optBtn.setAttribute("aria-expanded", "false");
optSheet.setAttribute("aria-hidden", "true"); optSheet.setAttribute("aria-hidden", "true");
} }
@ -123,26 +107,19 @@ function syncHeaderAuthUI() {
else openOptionsSheet(); else openOptionsSheet();
} }
// Click sul bottone: toggle (fase di cattura per battere eventuali altri handler)
optBtn.addEventListener("click", toggleOptionsSheet, { capture: true }); optBtn.addEventListener("click", toggleOptionsSheet, { capture: true });
// Chiudi clic overlay
overlayEl?.addEventListener("click", (e) => { overlayEl?.addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
closeOptionsSheet(); closeOptionsSheet();
}); });
// Chiudi con ESC
document.addEventListener("keydown", (e) => { document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && optSheet.classList.contains("open")) { if (e.key === "Escape" && optSheet.classList.contains("open")) {
closeOptionsSheet(); closeOptionsSheet();
} }
}); });
// Evita chiusure involontarie per click interni
optSheet.addEventListener("click", (e) => e.stopPropagation()); optSheet.addEventListener("click", (e) => e.stopPropagation());
// Espone una close per usarla altrove (es. dopo la scelta)
window.closeOptionsSheet = closeOptionsSheet; window.closeOptionsSheet = closeOptionsSheet;
})(); })();
@ -150,41 +127,36 @@ function syncHeaderAuthUI() {
// LOGIN AUTOMATICO SU INDEX // LOGIN AUTOMATICO SU INDEX
// =============================== // ===============================
document.addEventListener("DOMContentLoaded", async () => { document.addEventListener("DOMContentLoaded", async () => {
// Allinea subito lUI in base al token eventualmente già presente
syncHeaderAuthUI(); syncHeaderAuthUI();
try { try {
// 1) Carica config
const cfgRes = await fetch('/config'); const cfgRes = await fetch('/config');
const cfg = await cfgRes.json(); const cfg = await cfgRes.json();
window.BASE_URL = cfg.baseUrl; window.BASE_URL = cfg.baseUrl;
// 2) Recupera token salvato
const savedToken = localStorage.getItem("token"); const savedToken = localStorage.getItem("token");
// Se non c'è token → mostra login
if (!savedToken) { if (!savedToken) {
document.getElementById("loginModal").style.display = "flex"; document.getElementById("loginModal").style.display = "flex";
return; return;
} }
// 3) Verifica token
const ping = await fetch(`${window.BASE_URL}/photos`, { const ping = await fetch(`${window.BASE_URL}/photos`, {
headers: { "Authorization": "Bearer " + savedToken } headers: { "Authorization": "Bearer " + savedToken }
}); });
if (!ping.ok) { if (!ping.ok) {
// Token invalido → cancella e mostra login
localStorage.removeItem("token"); localStorage.removeItem("token");
syncHeaderAuthUI(); // riallinea header subito syncHeaderAuthUI();
document.getElementById("loginModal").style.display = "flex"; document.getElementById("loginModal").style.display = "flex";
return; return;
} }
// 4) Token valido → salva e carica gallery
window.token = savedToken; window.token = savedToken;
syncHeaderAuthUI(); // <— mostra il logout senza refresh syncHeaderAuthUI();
loadPhotos();
// 🔥 NON caricare più foto da main.js
// La gallery viene caricata da initGallery() in gallery.js
} catch (err) { } catch (err) {
console.error("Errore autenticazione:", err); console.error("Errore autenticazione:", err);
@ -193,62 +165,55 @@ document.addEventListener("DOMContentLoaded", async () => {
}); });
// =============================== // ===============================
// VARIABILI GLOBALI // SETTINGS (⚙️)
// =============================== // ===============================
let currentSort = "desc"; document.getElementById('settingsBtn')?.addEventListener('click', () => {
let currentGroup = "auto";
let currentFilter = null;
window.currentSort = currentSort;
window.currentGroup = currentGroup;
window.currentFilter = currentFilter;
// ===============================
// BOTTONI OPZIONI
// ===============================
document.querySelectorAll("#optionsSheet .sheet-btn").forEach(btn => {
btn.addEventListener("click", () => {
if (btn.dataset.sort) window.currentSort = currentSort = btn.dataset.sort;
if (btn.dataset.group) window.currentGroup = currentGroup = btn.dataset.group;
if (btn.dataset.filter) window.currentFilter = currentFilter = btn.dataset.filter;
// Chiudi sheet e overlay dopo la scelta (usa lAPI esposta sopra)
window.closeOptionsSheet?.();
refreshGallery();
});
});
// ===============================
// REFRESH GALLERY
// ===============================
function refreshGallery() {
console.log("Aggiornamento galleria...");
const data = Array.isArray(window.photosData) ? window.photosData : [];
let photos = [...data];
if (typeof applyFilters === 'function') photos = applyFilters(photos);
if (typeof sortByDate === 'function') photos = sortByDate(photos, currentSort);
let sections = [{ label: 'Tutte', photos }];
if (typeof groupByDate === 'function') sections = groupByDate(photos, currentGroup);
if (typeof renderGallery === 'function') {
renderGallery(sections);
}
}
window.refreshGallery = refreshGallery;
// ===============================
// SETTINGS (⚙️) — apre admin.html
// ===============================
const settingsBtn = document.getElementById('settingsBtn');
settingsBtn?.addEventListener('click', () => {
window.location.href = "admin.html"; 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 // LOGIN SUBMIT
// =============================== // ===============================
@ -282,9 +247,11 @@ document.getElementById("loginSubmit").addEventListener("click", async () => {
const loginModalEl = document.getElementById("loginModal"); const loginModalEl = document.getElementById("loginModal");
if (loginModalEl) loginModalEl.style.display = "none"; if (loginModalEl) loginModalEl.style.display = "none";
// Riallinea UI header subito (mostra logout) e carica gallery // Riallinea UI header subito
syncHeaderAuthUI(); syncHeaderAuthUI();
loadPhotos();
// 🔥 RICARICA LA PAGINA PER FAR PARTIRE initGallery()
window.location.reload();
} catch (err) { } catch (err) {
console.error("Errore login:", err); console.error("Errore login:", err);

116
public/js/state.js Normal file
View file

@ -0,0 +1,116 @@
// ===============================
// state.js — Stato condiviso + API
// ===============================
let localPhotos = [];
let photoDB = [];
// ---- LAST SYNC ----
function getLastSync() {
return localStorage.getItem("lastSync");
}
function setLastSync(ts) {
if (!ts) return;
localStorage.setItem("lastSync", ts);
}
// ---- STATO FOTO ----
function getLocalPhotos() {
return localPhotos;
}
function setLocalPhotos(photos) {
localPhotos = Array.isArray(photos) ? photos : [];
photoDB = [...localPhotos];
}
function addPhotoLocal(photo) {
if (!photo || !photo.id) return;
const idx = localPhotos.findIndex(p => p.id === photo.id);
if (idx >= 0) localPhotos[idx] = photo;
else localPhotos.push(photo);
}
function removePhotoLocal(id) {
const before = localPhotos.length;
localPhotos = localPhotos.filter(p => p.id !== id);
const after = localPhotos.length;
console.log("[removePhotoLocal] id:", id, "prima:", before, "dopo:", after);
}
function saveLocalState() {
try {
localStorage.setItem("photosCache", JSON.stringify(localPhotos));
} catch (e) {
console.warn("saveLocalState error:", e);
}
}
// ---- API ----
async function getAllPhotos() {
const token = localStorage.getItem("token");
console.log("[getAllPhotos] Fetch /photos...");
const res = await fetch("/photos", {
headers: {
"Authorization": "Bearer " + token
}
});
if (!res.ok) throw new Error("Errore getAllPhotos");
const data = await res.json();
const photos = data.photos || data || [];
console.log("[getAllPhotos] numero foto:", photos.length);
setLocalPhotos(photos);
return photos;
}
async function getPhotoById(id) {
const token = localStorage.getItem("token");
const q = `id=${encodeURIComponent(id)}`;
const res = await fetch(`/photos/byIds?${q}`, {
headers: {
"Authorization": "Bearer " + token
}
});
if (!res.ok) throw new Error("Errore getPhotoById");
const data = await res.json();
return data.photos || [];
}
async function getChanges(since, user) {
const token = localStorage.getItem("token");
const url = `/photos/changes?since=${encodeURIComponent(since)}&user=${encodeURIComponent(user)}`;
console.log("[getChanges] URL:", url);
const res = await fetch(url, {
headers: {
"Authorization": "Bearer " + token
}
});
if (!res.ok) throw new Error("Errore getChanges");
return await res.json();
}
// ---- EXPORT ----
window.getLastSync = getLastSync;
window.setLastSync = setLastSync;
window.getLocalPhotos = getLocalPhotos;
window.setLocalPhotos = setLocalPhotos;
window.addPhotoLocal = addPhotoLocal;
window.removePhotoLocal = removePhotoLocal;
window.saveLocalState = saveLocalState;
window.getAllPhotos = getAllPhotos;
window.getPhotoById = getPhotoById;
window.getChanges = getChanges;

247
public/js/sync.js Normal file
View file

@ -0,0 +1,247 @@
// ===============================================
// 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 {};
}
}
// ===============================================
// GET PHOTO BY ID — con log puliti
// ===============================================
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(`📥 [getPhotoById] Richiedo foto id=${id}`);
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(`📤 [getPhotoById] id=${id} → trovate ${json.length} foto`);
return json;
} catch (e) {
console.error("❌ [getPhotoById] ERRORE JSON:", e);
return [];
}
}
// ===============================================
// INCREMENTAL SYNC — versione completa e pulita
// ===============================================
async function incrementalSync() {
console.log("==============================================");
console.log("🚀 incrementalSync() START");
const payload = parseJwt(localStorage.getItem("token"));
const user = payload.name || "Common";
let lastSync = getLastSync();
const currentPhotos = getLocalPhotos();
console.log(`👤 Utente: ${user}`);
console.log(`🕒 lastSync: ${lastSync}`);
console.log(`📸 Foto locali attuali: ${currentPhotos.length}`);
// ============================================================
// 1) FULL LOAD
// ============================================================
if (!lastSync || currentPhotos.length === 0) {
console.log("🟦 FULL LOAD → caricamento completo del DB");
try {
const photos = await getAllPhotos();
console.log(`📥 FULL LOAD → ricevute ${photos.length} foto`);
saveLocalState();
refreshGallery();
const now = new Date().toISOString();
setLastSync(now);
console.log(`🕒 FULL LOAD → setLastSync = ${now}`);
console.log("🏁 incrementalSync() COMPLETATO (fullLoad)");
console.log("==============================================");
return;
} catch (err) {
console.error("❌ [incrementalSync] ERRORE in fullLoad:", err);
return;
}
}
// ============================================================
// 2) SYNC INCREMENTALE
// ============================================================
console.log("🟩 INCREMENTAL SYNC → controllo cambiamenti");
let changes;
try {
changes = await getChanges(lastSync, user);
} catch (err) {
console.error("❌ [incrementalSync] ERRORE getChanges:", err);
return;
}
console.log(`📥 Cambiamenti ricevuti: ${changes?.changes?.length || 0}`);
if (!changes || !changes.changes || changes.changes.length === 0) {
console.log("🟨 Nessun cambiamento → lastSync NON aggiornato");
return;
}
// ============================================================
// 3) APPLICA CAMBIAMENTI
// ============================================================
for (const ch of changes.changes) {
console.log("----------------------------------------------");
console.log(`🔄 Cambio:`, ch);
// FOTO AGGIUNTA
if (ch.change_type === "added") {
console.log(`🟢 Foto aggiunta → id=${ch.photo_id}`);
try {
const arr = await getPhotoById(ch.photo_id);
if (arr.length) {
console.log(`📸 Aggiungo foto id=${ch.photo_id} a localPhotos`);
addPhotoLocal(arr[0]);
} else {
console.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(`🔴 Foto rimossa → id=${ch.photo_id}`);
removePhotoLocal(ch.photo_id);
const el = document.getElementById("photo_" + ch.photo_id);
if (el) {
console.log(`🗑️ Rimuovo elemento DOM: ${el.id}`);
el.remove();
}
}
}
console.log(`📸 Foto locali dopo sync: ${getLocalPhotos().length}`);
// ============================================================
// 4) REFRESH UI
// ============================================================
console.log("🔄 refreshGallery()");
refreshGallery();
// ============================================================
// 5) AGGIORNA lastSync
// ============================================================
const lastTimestamp = changes.changes.at(-1).timestamp || new Date().toISOString();
console.log(`🕒 Aggiorno lastSync → ${lastTimestamp}`);
setLastSync(lastTimestamp);
console.log("🏁 incrementalSync() COMPLETATO");
console.log("==============================================");
}
// ===============================================
// WEBSOCKET REAL-TIME — versione pulita
// ===============================================
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);
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 → aggiorno 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;

253
public/js/sync.js.ok Normal file
View file

@ -0,0 +1,253 @@
// ===============================
// 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: 3.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

View file

@ -0,0 +1 @@
prova

File diff suppressed because one or more lines are too long

View file

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

File diff suppressed because it is too large Load diff

4
q.sh Normal file
View file

@ -0,0 +1,4 @@
inotifywait -m -r -e create,delete "./public/photos/Fabio/" |
while read path action file; do
echo "Evento: $action su $file"
done

192
routes/photos.js Normal file
View file

@ -0,0 +1,192 @@
// 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: "photo_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) => {
console.log("📤 [GET /photos] Richiesta ricevuta");
try {
const rows = await db('photos')
.select('*')
.orderBy('mtimeMs', 'desc');
console.log(`📤 [GET /photos] 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;

54
scan.sh Normal file
View file

@ -0,0 +1,54 @@
#!/bin/bash
USERS_FILE="./api_v1/users.json"
PHOTOS_ROOT="./public/photos"
JWT_SECRET="123456789"
NODE_ENDPOINT="https://prova.patachina.it/scan"
# 1. Legge gli utenti dal JSON
USERS=($(jq -r '.users[].name' "$USERS_FILE"))
WATCH_PATHS=()
for user in "${USERS[@]}"; do
if [[ "$user" == "Admin" ]]; then
WATCH_PATHS+=("$PHOTOS_ROOT/Common/original")
else
WATCH_PATHS+=("$PHOTOS_ROOT/$user/original")
fi
done
echo "Monitoro:"
printf '%s\n' "${WATCH_PATHS[@]}"
# 2. Funzione per generare JWT per un utente specifico
generate_jwt() {
local user="$1"
local header payload signature
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 -w0 | tr '+/' '-_' | tr -d '=')
payload=$(echo -n "{\"name\":\"$user\",\"email\":\"$user@system\",\"id\":999,\"ts\":$(date +%s)}" \
| base64 -w0 | tr '+/' '-_' | tr -d '=')
signature=$(echo -n "$header.$payload" \
| openssl dgst -sha256 -hmac "$JWT_SECRET" -binary \
| base64 -w0 | tr '+/' '-_' | tr -d '=')
echo "$header.$payload.$signature"
}
# 3. Avvia inotifywait su tutte le directory
inotifywait -m -r -e create,modify,delete "${WATCH_PATHS[@]}" |
while read fullpath action file; do
echo "Evento: $action su $file"
# 4. Estrae lutente dal path
# Esempio: /photos/Fabio/originale/IMG001.jpg → Fabio
user=$(echo "$fullpath" | awk -F'/' '{print $(NF-2)}')
echo "Utente rilevato: $user"
JWT=$(generate_jwt "$user")
curl -X GET "$NODE_ENDPOINT" \
-H "Authorization: Bearer $JWT"
done

296
server.js Normal file
View file

@ -0,0 +1,296 @@
// server.js
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const scanPhoto = require('./api_v1/scanner/scanPhoto.js');
const { WEB_ROOT } = require('./api_v1/config');
const config = require('./api_v1/config');
const db = require('./db/knex');
const photosRouter = require('./routes/photos');
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
const PORT = process.env.SERVER_PORT || 4000;
const server = express();
// STATIC
server.use(express.static(path.join(__dirname, 'public')));
// BODY PARSER
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
// CONFIG PUBBLICO
server.get('/config', (req, res) => {
res.json({
baseUrl: config.BASE_URL,
pathFull: config.PATH_FULL,
galleryRefreshSeconds: config.GALLERY_REFRESH_SECONDS
});
});
// USERS DB
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8'));
// JWT
function createToken(payload) {
return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN });
}
const denylist = new Map();
function addToDenylist(token) {
try {
const decoded = jwt.decode(token);
const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60;
denylist.set(token, exp);
} catch {
denylist.set(token, Math.floor(Date.now() / 1000) + 60);
}
}
function isRevoked(token) {
const exp = denylist.get(token);
if (!exp) return false;
if (exp * 1000 < Date.now()) {
denylist.delete(token);
return false;
}
return true;
}
function verifyToken(token) {
return jwt.verify(token, SECRET_KEY);
}
// HOME
server.get('/', (req, res) => {
res.sendFile(path.resolve('public/index.html'));
});
// LOGIN
server.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = userdb.users.find((u) => u.email === email);
if (!user) {
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
}
const ok = await bcrypt.compare(password, user.password);
if (!ok) {
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
}
const token = createToken({
id: user.id,
email: user.email,
name: user.name,
});
res.status(200).json({
token,
name: user.name,
});
});
// LOGOUT
server.post('/auth/logout', (req, res) => {
const auth = req.headers.authorization || '';
const [scheme, token] = auth.split(' ');
if (scheme === 'Bearer' && token) {
addToDenylist(token);
}
return res.status(204).end();
});
// JWT MIDDLEWARE
server.use(/^(?!\/auth).*$/, (req, res, next) => {
const auth = req.headers.authorization || '';
const [scheme, token] = auth.split(' ');
if (scheme !== 'Bearer' || !token) {
return res.status(401).json({ status: 401, message: 'Bad authorization header' });
}
if (isRevoked(token)) {
return res.status(401).json({ status: 401, message: 'Token revoked' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (err) {
return res
.status(401)
.json({ status: 401, message: 'Error: access_token is not valid' });
}
});
// FILTRO USER (GET)
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']));
}
next();
});
// SCAN FOTO
server.get('/scan', async (req, res) => {
try {
if (req.user && req.user.name === 'Admin') {
await scanPhoto(undefined, 'Admin', db);
return res.send({
status: 'Scansione completata',
user: 'Admin',
scope: 'tutti gli utenti + Common',
});
}
await scanPhoto(undefined, 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;
console.log("=== SUPER_SCAN EVENT ===");
console.log("TYPE:", type);
console.log("FILE:", file);
console.log("PATH:", path);
console.log("USER:", user);
console.log("========================");
res.json({ ok: true });
});
// FILE STATICI
server.get('/files', (req, res) => {
const requested = req.query.file || '';
const publicDir = path.resolve(path.join(__dirname, 'public'));
const resolved = path.resolve(publicDir, requested);
if (!resolved.startsWith(publicDir)) {
return res.status(400).json({ error: 'Invalid path' });
}
if (!fs.existsSync(resolved) || fs.statSync(resolved).isDirectory()) {
return res.status(404).json({ error: 'Not found' });
}
res.sendFile(resolved);
});
// RESET DB MANUALE
server.get('/initDB', async (req, res) => {
try {
await db('photos').del();
res.json({
status: 'DB resettato',
indexRemoved: false
});
} catch (err) {
console.error('initDB: errore generale:', err);
res.status(500).json({ status: 'errore', message: err.message });
}
});
// DELETE FOTO da DB + thumbs
server.delete('/delphoto/:id', async (req, res) => {
const { id } = req.params;
const trx = await db.transaction();
try {
const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup');
const { deleteThumbsById } = createCleanupFunctions(db);
const deletedThumbs = await deleteThumbsById(id);
const deletedDB = await trx('photos').where({ id }).del();
await trx.commit();
return res.json({
ok: true,
id,
deletedThumbs,
deletedDB: deletedDB > 0,
deletedIndex: false
});
} catch (err) {
await trx.rollback();
console.error('DELPHOTO errore:', err);
return res.status(500).json({ ok: false, error: err.message });
}
});
// RESET DB SOLO PER UN UTENTE
server.get('/initDBuser', async (req, res) => {
try {
let targetUser = req.user.name;
if (req.user.name === 'Admin') {
targetUser = req.query.user;
if (!targetUser) {
return res.status(400).json({
error: "Admin deve specificare ?user=<nome>"
});
}
}
const trx = await db.transaction();
const deleted = await trx('photos').where({ user: targetUser }).del();
await trx.commit();
res.json({
status: "OK",
user: targetUser,
deletedRecords: deleted
});
} catch (err) {
console.error("initDBuser errore:", err);
res.status(500).json({ error: "Errore durante initDBuser", details: err.message });
}
});
// ROUTER PHOTOS (SQLite)
server.use('/photos', photosRouter);
// START SERVER
server.listen(PORT, () => {
console.log(`Auth API server running on port ${PORT} ...`);
});
// Pulizia denylist
setInterval(() => {
const nowSec = Math.floor(Date.now() / 1000);
for (const [tok, exp] of denylist.entries()) {
if (exp < nowSec) denylist.delete(tok);
}
}, 60 * 1000);

296
server.js.ok Normal file
View file

@ -0,0 +1,296 @@
// server.js
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const scanPhoto = require('./api_v1/scanner/scanPhoto.js');
const { WEB_ROOT } = require('./api_v1/config');
const config = require('./api_v1/config');
const db = require('./db/knex');
const photosRouter = require('./routes/photos');
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
const PORT = process.env.SERVER_PORT || 4000;
const server = express();
// STATIC
server.use(express.static(path.join(__dirname, 'public')));
// BODY PARSER
server.use(express.json());
server.use(express.urlencoded({ extended: true }));
// CONFIG PUBBLICO
server.get('/config', (req, res) => {
res.json({
baseUrl: config.BASE_URL,
pathFull: config.PATH_FULL,
galleryRefreshSeconds: config.GALLERY_REFRESH_SECONDS
});
});
// USERS DB
const userdb = JSON.parse(fs.readFileSync('./api_v1/users.json', 'utf-8'));
// JWT
function createToken(payload) {
return jwt.sign(payload, SECRET_KEY, { expiresIn: EXPIRES_IN });
}
const denylist = new Map();
function addToDenylist(token) {
try {
const decoded = jwt.decode(token);
const exp = decoded?.exp || Math.floor(Date.now() / 1000) + 60;
denylist.set(token, exp);
} catch {
denylist.set(token, Math.floor(Date.now() / 1000) + 60);
}
}
function isRevoked(token) {
const exp = denylist.get(token);
if (!exp) return false;
if (exp * 1000 < Date.now()) {
denylist.delete(token);
return false;
}
return true;
}
function verifyToken(token) {
return jwt.verify(token, SECRET_KEY);
}
// HOME
server.get('/', (req, res) => {
res.sendFile(path.resolve('public/index.html'));
});
// LOGIN
server.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = userdb.users.find((u) => u.email === email);
if (!user) {
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
}
const ok = await bcrypt.compare(password, user.password);
if (!ok) {
return res.status(401).json({ status: 401, message: 'Incorrect email or password' });
}
const token = createToken({
id: user.id,
email: user.email,
name: user.name,
});
res.status(200).json({
token,
name: user.name,
});
});
// LOGOUT
server.post('/auth/logout', (req, res) => {
const auth = req.headers.authorization || '';
const [scheme, token] = auth.split(' ');
if (scheme === 'Bearer' && token) {
addToDenylist(token);
}
return res.status(204).end();
});
// JWT MIDDLEWARE
server.use(/^(?!\/auth).*$/, (req, res, next) => {
const auth = req.headers.authorization || '';
const [scheme, token] = auth.split(' ');
if (scheme !== 'Bearer' || !token) {
return res.status(401).json({ status: 401, message: 'Bad authorization header' });
}
if (isRevoked(token)) {
return res.status(401).json({ status: 401, message: 'Token revoked' });
}
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (err) {
return res
.status(401)
.json({ status: 401, message: 'Error: access_token is not valid' });
}
});
// FILTRO USER (GET)
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']));
}
next();
});
// SCAN FOTO
server.get('/scan', async (req, res) => {
try {
if (req.user && req.user.name === 'Admin') {
await scanPhoto(undefined, 'Admin', db);
return res.send({
status: 'Scansione completata',
user: 'Admin',
scope: 'tutti gli utenti + Common',
});
}
await scanPhoto(undefined, 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;
console.log("=== SUPER_SCAN EVENT ===");
console.log("TYPE:", type);
console.log("FILE:", file);
console.log("PATH:", path);
console.log("USER:", user);
console.log("========================");
res.json({ ok: true });
});
// FILE STATICI
server.get('/files', (req, res) => {
const requested = req.query.file || '';
const publicDir = path.resolve(path.join(__dirname, 'public'));
const resolved = path.resolve(publicDir, requested);
if (!resolved.startsWith(publicDir)) {
return res.status(400).json({ error: 'Invalid path' });
}
if (!fs.existsSync(resolved) || fs.statSync(resolved).isDirectory()) {
return res.status(404).json({ error: 'Not found' });
}
res.sendFile(resolved);
});
// RESET DB MANUALE
server.get('/initDB', async (req, res) => {
try {
await db('photos').del();
res.json({
status: 'DB resettato',
indexRemoved: false
});
} catch (err) {
console.error('initDB: errore generale:', err);
res.status(500).json({ status: 'errore', message: err.message });
}
});
// DELETE FOTO da DB + thumbs
server.delete('/delphoto/:id', async (req, res) => {
const { id } = req.params;
const trx = await db.transaction();
try {
const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup');
const { deleteThumbsById } = createCleanupFunctions(db);
const deletedThumbs = await deleteThumbsById(id);
const deletedDB = await trx('photos').where({ id }).del();
await trx.commit();
return res.json({
ok: true,
id,
deletedThumbs,
deletedDB: deletedDB > 0,
deletedIndex: false
});
} catch (err) {
await trx.rollback();
console.error('DELPHOTO errore:', err);
return res.status(500).json({ ok: false, error: err.message });
}
});
// RESET DB SOLO PER UN UTENTE
server.get('/initDBuser', async (req, res) => {
try {
let targetUser = req.user.name;
if (req.user.name === 'Admin') {
targetUser = req.query.user;
if (!targetUser) {
return res.status(400).json({
error: "Admin deve specificare ?user=<nome>"
});
}
}
const trx = await db.transaction();
const deleted = await trx('photos').where({ user: targetUser }).del();
await trx.commit();
res.json({
status: "OK",
user: targetUser,
deletedRecords: deleted
});
} catch (err) {
console.error("initDBuser errore:", err);
res.status(500).json({ error: "Errore durante initDBuser", details: err.message });
}
});
// ROUTER PHOTOS (SQLite)
server.use('/photos', photosRouter);
// START SERVER
server.listen(PORT, () => {
console.log(`Auth API server running on port ${PORT} ...`);
});
// Pulizia denylist
setInterval(() => {
const nowSec = Math.floor(Date.now() / 1000);
for (const [tok, exp] of denylist.entries()) {
if (exp < nowSec) denylist.delete(tok);
}
}, 60 * 1000);

8
w.sh Normal file
View file

@ -0,0 +1,8 @@
#!/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 Normal file
View file

@ -0,0 +1,22 @@
#!/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 Normal file
View file

@ -0,0 +1,22 @@
#!/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

39
watcher_logic.mjs Normal file
View file

@ -0,0 +1,39 @@
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);
});

35
watcher_logic1.mjs Normal file
View file

@ -0,0 +1,35 @@
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}`);
}
});

64
watcher_logic2.mjs Normal file
View file

@ -0,0 +1,64 @@
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}`);
});

84
watcher_logic3.mjs Normal file
View file

@ -0,0 +1,84 @@
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}`);
});

154
watcher_logic4.mjs Normal file
View file

@ -0,0 +1,154 @@
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();
});

128
ws-server.js Normal file
View file

@ -0,0 +1,128 @@
// ws-server.js
require("dotenv").config();
const WebSocket = require("ws");
const jwt = require("jsonwebtoken");
// -----------------------------------------------------
// CONFIGURAZIONE DA .env
// -----------------------------------------------------
const PORT = process.env.WS_PORT || 4002;
const HOST = process.env.WS_HOST || "0.0.0.0";
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
console.error("❌ ERRORE: JWT_SECRET non definito nel .env");
process.exit(1);
}
// -----------------------------------------------------
// AVVIO WEBSOCKET SERVER
// -----------------------------------------------------
const wss = new WebSocket.Server({
port: PORT,
host: HOST
});
console.log(`🚀 WebSocket server attivo su ws://${HOST}:${PORT}`);
// -----------------------------------------------------
// GESTIONE CONNESSIONI
// -----------------------------------------------------
wss.on("connection", (ws, req) => {
console.log("🔌 Nuovo client connesso");
ws.isAlive = true;
ws.on("pong", () => {
ws.isAlive = true;
});
ws.on("message", msg => {
let data;
try {
data = JSON.parse(msg);
} catch (err) {
console.error("❌ Errore parsing JSON:", err);
return;
}
// -----------------------------------------------------
// AUTENTICAZIONE JWT
// -----------------------------------------------------
if (data.type === "auth") {
try {
const payload = jwt.verify(data.token, JWT_SECRET);
ws.user = payload.name; // es: "Fabio"
ws.authenticated = true;
ws.send(JSON.stringify({
type: "auth_ok",
user: ws.user
}));
console.log(`🔐 Client autenticato: ${ws.user}`);
} catch (err) {
ws.send(JSON.stringify({
type: "auth_error",
error: err.message
}));
ws.close();
}
return;
}
// -----------------------------------------------------
// MESSAGGI NON AUTORIZZATI
// -----------------------------------------------------
if (!ws.authenticated) {
ws.send(JSON.stringify({
type: "error",
message: "Not authenticated"
}));
return;
}
// -----------------------------------------------------
// MESSAGGI DAL CLIENT (se vuoi gestirli)
// -----------------------------------------------------
console.log(`📨 Messaggio da ${ws.user}:`, data);
});
ws.on("close", () => {
console.log(`❌ Client disconnesso: ${ws.user || "sconosciuto"}`);
});
});
// -----------------------------------------------------
// HEARTBEAT (evita connessioni zombie)
// -----------------------------------------------------
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) {
console.log(`💀 Connessione zombie terminata: ${ws.user}`);
return ws.terminate();
}
ws.isAlive = false;
ws.ping();
});
}, 30000);
// -----------------------------------------------------
// BROADCAST FILTRATO PER UTENTE
// -----------------------------------------------------
wss.broadcastToUser = function (user, msg) {
const json = JSON.stringify(msg);
for (const client of wss.clients) {
if (
client.readyState === WebSocket.OPEN &&
client.authenticated &&
client.user === user
) {
client.send(json);
}
}
};
module.exports = wss;

22
ww.sh Normal file
View file

@ -0,0 +1,22 @@
#!/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 Normal file
View file

@ -0,0 +1,22 @@
#!/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