okk
1
.Fabio.last_event
Normal file
|
|
@ -0,0 +1 @@
|
|||
1774133507738
|
||||
19
.env
|
|
@ -1,12 +1,29 @@
|
|||
BASE_URL=https://prova.patachina.it
|
||||
SERVER_PORT=4000
|
||||
|
||||
EMAIL=fabio@gmail.com
|
||||
PASSWORD=master66
|
||||
|
||||
JWT_SECRET=123456789
|
||||
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
|
||||
|
||||
# Logging
|
||||
LOG_MODE=both # console | file | both
|
||||
LOG_FILE=scan.log # nome file log
|
||||
LOG_FILE=scan.log
|
||||
LOG_DIR=public
|
||||
LOG_VERBOSE=true
|
||||
|
||||
|
||||
GALLERY_REFRESH_SECONDS=30
|
||||
|
||||
WS_PORT=4002
|
||||
WS_HOST=0.0.0.0
|
||||
|
|
|
|||
1
Fabio.scan
Normal file
|
|
@ -0,0 +1 @@
|
|||
lock
|
||||
1
a.jpg
Normal file
|
|
@ -0,0 +1 @@
|
|||
prova
|
||||
4
api_v1/admin_secret.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"email": "admin@gmail.com",
|
||||
"password": "master66"
|
||||
}
|
||||
|
|
@ -1,17 +1,25 @@
|
|||
require('dotenv').config();
|
||||
const path = require('path');
|
||||
|
||||
module.exports = {
|
||||
BASE_URL: process.env.BASE_URL,
|
||||
|
||||
EMAIL: process.env.EMAIL,
|
||||
PASSWORD: process.env.PASSWORD,
|
||||
|
||||
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',
|
||||
|
||||
// PATH_FULL ora è un boolean, non un path
|
||||
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([
|
||||
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
|
||||
'.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
|
|
@ -1 +0,0 @@
|
|||
{"photos":[]}
|
||||
|
|
@ -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 };
|
||||
|
|
@ -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 };
|
||||
|
|
@ -1,62 +1,66 @@
|
|||
// scanner/orphanCleanup.js
|
||||
const fs = require('fs');
|
||||
// api_v1/scanner/orphanCleanup.js
|
||||
const fsp = require('fs/promises');
|
||||
const path = require('path');
|
||||
const { WEB_ROOT, INDEX_PATH } = require('../config');
|
||||
|
||||
module.exports = function createCleanupFunctions(db) {
|
||||
|
||||
// 1) Recupera gli ID dal DB per una cartella specifica
|
||||
async function buildIdsListForFolder(userName, cartella) {
|
||||
const indexPath = path.resolve(__dirname, '..', '..', WEB_ROOT, INDEX_PATH);
|
||||
const idsIndex = [];
|
||||
|
||||
if (!fs.existsSync(indexPath)) return idsIndex;
|
||||
|
||||
try {
|
||||
const raw = await fsp.readFile(indexPath, 'utf8');
|
||||
const index = JSON.parse(raw);
|
||||
const rows = await db("photos")
|
||||
.select("id")
|
||||
.where({ user: userName, cartella });
|
||||
|
||||
const folder = index?.[userName]?.[cartella];
|
||||
if (!folder) return idsIndex;
|
||||
|
||||
return Object.keys(folder).filter(k => k !== "_folderHash");
|
||||
} catch {
|
||||
return idsIndex;
|
||||
return rows.map(r => r.id);
|
||||
} catch (err) {
|
||||
console.error("Errore buildIdsListForFolder:", err);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Rimuove un ID dalla lista (invariati)
|
||||
function removeIdFromList(idsIndex, id) {
|
||||
return idsIndex.filter(x => x !== id);
|
||||
}
|
||||
|
||||
// 3) Cancella thumbs dal filesystem usando SQLite
|
||||
async function deleteThumbsById(id) {
|
||||
const col = db.get('photos');
|
||||
const rec = col.find({ id }).value();
|
||||
const rec = await db('photos').where({ id }).first();
|
||||
if (!rec) return false;
|
||||
|
||||
const thumbs = [rec.thub1, rec.thub2].filter(Boolean);
|
||||
let deleted = false;
|
||||
|
||||
for (const t of thumbs) {
|
||||
const abs = path.resolve(__dirname, '../public' + t);
|
||||
if (fs.existsSync(abs)) {
|
||||
const abs = path.resolve(__dirname, '..', '..', 'public', t);
|
||||
try {
|
||||
await fsp.rm(abs, { force: true });
|
||||
console.log(` 🔴 Thumb eliminato: ${abs}`);
|
||||
deleted = true;
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return deleted;
|
||||
}
|
||||
|
||||
function deleteFromDB(id) {
|
||||
const col = db.get('photos');
|
||||
const exists = col.find({ id }).value();
|
||||
// 4) Cancella dal DB SQLite + registra in photo_changes
|
||||
async function deleteFromDB(id, userName) {
|
||||
const deleted = await db('photos').where({ id }).del();
|
||||
|
||||
if (exists) {
|
||||
col.remove({ id }).write();
|
||||
if (deleted > 0) {
|
||||
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 false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,60 +1,24 @@
|
|||
const axios = require('axios');
|
||||
const { BASE_URL, EMAIL, PASSWORD, SEND_PHOTOS } = require('../config');
|
||||
// scanner/postWithAuth.js
|
||||
// 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;
|
||||
if (cachedToken && !force) return cachedToken;
|
||||
|
||||
try {
|
||||
const res = await axios.post(`${BASE_URL}/auth/login`, {
|
||||
email: EMAIL,
|
||||
password: PASSWORD
|
||||
});
|
||||
|
||||
cachedToken = res.data.token;
|
||||
return cachedToken;
|
||||
|
||||
} catch (err) {
|
||||
console.error('ERRORE LOGIN:', err.message);
|
||||
return null;
|
||||
/**
|
||||
* Inserisce o aggiorna un record nel DB SQLite
|
||||
* (sostituisce completamente axios.post)
|
||||
*/
|
||||
async function postToDB(record) {
|
||||
if (!record || !record.id) {
|
||||
throw new Error("Record non valido");
|
||||
}
|
||||
}
|
||||
|
||||
async function postWithAuth(url, payload) {
|
||||
if (!SEND_PHOTOS) return;
|
||||
await db('photos')
|
||||
.insert(record)
|
||||
.onConflict('id')
|
||||
.merge();
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = postWithAuth;
|
||||
|
||||
return postToDB;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
// scanner/scanCartella.js
|
||||
// api_v1/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) {
|
||||
/**
|
||||
* scanCartella: genera informazioni sui file nella cartella
|
||||
* Non esegue processFile né 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 = '') {
|
||||
let entries = [];
|
||||
|
|
@ -27,40 +36,25 @@ async function* scanCartella(userName, cartella, absCartella, previousIndexTree)
|
|||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||
|
||||
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
|
||||
|
||||
// ID deterministico (stesso criterio di prima)
|
||||
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
||||
|
||||
// Stat veloce
|
||||
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,
|
||||
name: e.name,
|
||||
relPath: fileRelPath,
|
||||
absPath,
|
||||
ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
|
||||
meta._indexHash = hash;
|
||||
|
||||
yield meta;
|
||||
stat: st,
|
||||
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -1,26 +1,24 @@
|
|||
// scanner/scanPhoto.js
|
||||
// api_v1/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 processFile = require('./processFile');
|
||||
const postWithAuth = require('./postWithAuth');
|
||||
const { sha256 } = require('./utils');
|
||||
|
||||
const {
|
||||
WEB_ROOT,
|
||||
SEND_PHOTOS,
|
||||
BASE_URL,
|
||||
WRITE_INDEX,
|
||||
SUPPORTED_EXTS
|
||||
} = require('../config');
|
||||
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
|
||||
// Variabile per log completo o ridotto
|
||||
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
|
||||
|
||||
// Formatta ms → HH:MM:SS
|
||||
function formatTime(ms) {
|
||||
const sec = Math.floor(ms / 1000);
|
||||
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
||||
|
|
@ -29,9 +27,6 @@ function formatTime(ms) {
|
|||
return `${h}:${m}:${s}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ⚡ CONTEGGIO FILE VELOCE (senza hashing, EXIF, sharp, ecc.)
|
||||
// ---------------------------------------------------------
|
||||
async function countFilesFast(rootDir) {
|
||||
let count = 0;
|
||||
|
||||
|
|
@ -65,30 +60,14 @@ 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 { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||
const userDir = path.join(photosRoot, userName, 'original');
|
||||
|
||||
let totalNew = 0;
|
||||
let totalDeleted = 0;
|
||||
let totalUnchanged = 0;
|
||||
|
||||
let newFiles = [];
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 1) ⚡ CONTEGGIO FILE SUPER VELOCE
|
||||
// ---------------------------------------------------------
|
||||
const TOTAL_FILES = await countFilesFast(userDir);
|
||||
|
||||
if (TOTAL_FILES === 0) {
|
||||
log(`Nessun file trovato per user=${userName}`);
|
||||
return [];
|
||||
|
|
@ -98,13 +77,10 @@ async function scanPhoto(dir, userName, db) {
|
|||
|
||||
let CURRENT = 0;
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Funzione per aggiornare il file statico leggibile dall’HTML
|
||||
// ---------------------------------------------------------
|
||||
async function updateStatusFile() {
|
||||
const now = Date.now();
|
||||
const elapsedMs = now - start;
|
||||
const avg = elapsedMs / CURRENT;
|
||||
const avg = elapsedMs / Math.max(CURRENT, 1);
|
||||
const remainingMs = (TOTAL_FILES - CURRENT) * avg;
|
||||
|
||||
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) {
|
||||
log(`📁 Inizio cartella: ${cartella}`);
|
||||
|
||||
let idsIndex = await buildIdsListForFolder(user, cartella);
|
||||
const rows = await db('photos')
|
||||
.where({ user, cartella })
|
||||
.select('id');
|
||||
|
||||
let newCount = 0;
|
||||
let unchangedCount = 0;
|
||||
let deletedCount = 0;
|
||||
const idsSet = new Set(rows.map(r => r.id));
|
||||
|
||||
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
|
||||
for await (const f of scanCartella(user, cartella, absCartella, db)) {
|
||||
CURRENT++;
|
||||
const fileName = m.path.split('/').pop();
|
||||
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
|
||||
|
||||
// Aggiorna file statico per l’HTML
|
||||
await updateStatusFile();
|
||||
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
|
||||
const fileName = f.name;
|
||||
const id = f.id;
|
||||
const st = f.stat;
|
||||
|
||||
// ⚪ FILE INVARIATO
|
||||
if (m.unchanged) {
|
||||
if (LOG_VERBOSE) {
|
||||
|
||||
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}`);
|
||||
}
|
||||
idsIndex = removeIdFromList(idsIndex, m.id);
|
||||
unchangedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 🟢 FILE NUOVO O MODIFICATO
|
||||
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// PATH CAMBIATO = NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (prev && prev.path !== f.path) {
|
||||
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;
|
||||
prev = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SCAN SOTTOCARTELLE
|
||||
// NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (!prev) {
|
||||
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
// INSERT CORRETTO (senza colonna gps)
|
||||
await db("photos").insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'added',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
idsSet.delete(id);
|
||||
|
||||
newFiles.push(meta);
|
||||
//log(`🟢 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-SIZE-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.size_bytes === st.size) {
|
||||
//log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
|
||||
idsSet.delete(id);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-HASH-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.fast_hash === fastHash) {
|
||||
//log(`🔵 [FAST-HASH-SKIP] id=${id}`);
|
||||
idsSet.delete(id);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MODIFICATO
|
||||
// ---------------------------------------------------------
|
||||
//log(`🟠 [FULL-SCAN] id=${id}`);
|
||||
log(`${prefix} 🟠 Nuovo/Modificato: ${fileName}`);
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
await db("photos")
|
||||
.insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
})
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'updated',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
idsSet.delete(id);
|
||||
newFiles.push(meta);
|
||||
log(`${prefix} 🟠 Nuovo/Modificato al server ${fileName}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ORFANI
|
||||
// ---------------------------------------------------------
|
||||
for (const orphanId of idsSet) {
|
||||
log(` 🔴 Cancellato ${orphanId}`);
|
||||
await deleteThumbsById(orphanId);
|
||||
await deleteFromDB(orphanId, user);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SCAN DI TUTTE LE CARTELLE
|
||||
// ---------------------------------------------------------
|
||||
let entries = [];
|
||||
try {
|
||||
|
|
@ -211,35 +316,26 @@ async function scanPhoto(dir, userName, db) {
|
|||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SALVO INDEX
|
||||
// ---------------------------------------------------------
|
||||
if (WRITE_INDEX) {
|
||||
await saveIndex(nextIndexTree);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// INVIO AL SERVER / POPOLAZIONE DB
|
||||
// INVIO AL SERVER REMOTO
|
||||
// ---------------------------------------------------------
|
||||
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
|
||||
log(`📤 [SEND START] newFiles=${newFiles.length}`);
|
||||
|
||||
for (const p of newFiles) {
|
||||
const fileName = p.path.split('/').pop();
|
||||
|
||||
try {
|
||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||
log(`📤 Inviato al server: ${fileName}`);
|
||||
log(`📥 [SENT TO SERVER] ${p.name}`);
|
||||
} 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);
|
||||
|
||||
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
|
||||
log(`⏱ Tempo totale: ${elapsed}s`);
|
||||
log(`🟣 Scan COMPLETATO in ${elapsed}s`);
|
||||
|
||||
return newFiles;
|
||||
}
|
||||
|
|
|
|||
349
api_v1/scanner/scanPhoto.js.long_log
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
647
api_v1/server.js
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
prova
|
||||
86
db/init.js
Normal 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
1798
package-lock.json
generated
28
package.json
|
|
@ -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": {
|
||||
"async": "^3.2.6",
|
||||
"axios": "^1.13.5",
|
||||
"axios": "^1.13.6",
|
||||
"bcrypt": "^6.0.0",
|
||||
"body-parser": "^2.2.2",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"exifreader": "^4.36.2",
|
||||
"json-server": "^0.17.4",
|
||||
"exifreader": "^4.37.0",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"path": "^0.12.7",
|
||||
"sharp": "^0.34.5"
|
||||
"knex": "^3.1.0",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,15 @@
|
|||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#changesBox {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #aaa;
|
||||
border-radius: 5px;
|
||||
background: #f7f7f7;
|
||||
width: 350px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
|
|
@ -40,8 +49,19 @@
|
|||
<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="searchPhotoById()">Cerca Foto (nuovo /byIds)</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 -->
|
||||
<div id="progressContainer">
|
||||
<div id="progressBar"></div>
|
||||
|
|
@ -76,6 +96,30 @@ if (!token) {
|
|||
// 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() {
|
||||
const id = prompt("Inserisci l'ID della foto da cancellare:");
|
||||
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;
|
||||
|
||||
async function scan() {
|
||||
// Avvia lo scan sul backend
|
||||
fetch(`${BASE_URL}/scan`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
||||
// Avvia il polling dello stato
|
||||
startScanStatusPolling();
|
||||
}
|
||||
|
||||
|
|
@ -173,23 +244,19 @@ async function startScanStatusPolling() {
|
|||
|
||||
scanInterval = setInterval(async () => {
|
||||
try {
|
||||
// PERCORSO CORRETTO
|
||||
const res = await fetch(`/photos/scan_status.json?ts=` + Date.now());
|
||||
if (!res.ok) return;
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Aggiorna testo
|
||||
document.getElementById("scanProgress").textContent =
|
||||
`Progresso: ${data.current}/${data.total} (${data.percent}%)`;
|
||||
|
||||
document.getElementById("scanEta").textContent =
|
||||
`Tempo stimato rimanente: ${data.eta}`;
|
||||
|
||||
// Aggiorna barra
|
||||
document.getElementById("progressBar").style.width = data.percent + "%";
|
||||
|
||||
// Fine scan
|
||||
if (data.current >= data.total && data.total > 0) {
|
||||
clearInterval(scanInterval);
|
||||
scanInterval = null;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -45,7 +45,6 @@
|
|||
title="Logout"
|
||||
aria-label="Logout">
|
||||
|
||||
<!-- Icona PNG -->
|
||||
<img
|
||||
class="logout-icon"
|
||||
src="img/switch.png"
|
||||
|
|
@ -70,7 +69,6 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-close" id="modalClose">×</div>
|
||||
|
||||
<!-- Frecce navigazione -->
|
||||
<button class="modal-nav-btn prev" id="modalPrev" type="button" aria-label="Precedente"><</button>
|
||||
<button class="modal-nav-btn next" id="modalNext" type="button" aria-label="Successiva">></button>
|
||||
|
||||
|
|
@ -143,13 +141,12 @@
|
|||
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||
|
||||
<!-- 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 -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||
<script>
|
||||
eruda.init();
|
||||
console.log("Eruda inizializzato");
|
||||
</script>
|
||||
|
||||
<!-- Debug immediato -->
|
||||
|
|
@ -163,12 +160,16 @@
|
|||
<script src="js/gallery.js"></script>
|
||||
<script src="js/modal.js"></script>
|
||||
<script src="js/infoPanel.js"></script>
|
||||
|
||||
<!-- DEVE ESSERE PRIMA DI mapGlobal.js -->
|
||||
<script src="js/bottomSheet.js"></script>
|
||||
|
||||
<script src="js/mapGlobal.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>
|
||||
|
||||
</body>
|
||||
|
|
|
|||
27
public/js/api.js
Normal 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)}`);
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
// ===============================
|
||||
// GALLERY — completa, stile Google Photos
|
||||
// - Ordinamento
|
||||
// - Filtri
|
||||
// - Raggruppamento (auto/giorno/mese/anno)
|
||||
// - Render a sezioni
|
||||
// - Click: openModalFromList(sezione, indice) se disponibile (fallback openModal)
|
||||
// GALLERY — con SYNC INCREMENTALE + REFRESH DA .ENV
|
||||
// ===============================
|
||||
|
||||
// ORDINAMENTO
|
||||
let currentUser = null;
|
||||
let refreshSeconds = 30;
|
||||
|
||||
// ===============================
|
||||
// 3. ORDINAMENTO
|
||||
// ===============================
|
||||
function sortByDate(photos, direction = "desc") {
|
||||
return photos.slice().sort((a, b) => {
|
||||
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) {
|
||||
if (!window.currentFilter) return photos;
|
||||
|
||||
switch (window.currentFilter) {
|
||||
case "folder":
|
||||
return photos.filter(p => p.folder || (p.path && p.path.includes('/photos/')));
|
||||
return photos.filter(p => p.cartella);
|
||||
case "location":
|
||||
return photos.filter(p => p?.gps && p.gps.lat);
|
||||
return photos.filter(p => p.lat && p.lon);
|
||||
case "type":
|
||||
return photos.filter(p => p?.mime_type && p.mime_type.startsWith("image/"));
|
||||
return photos.filter(p => p?.mime_type?.startsWith("image/"));
|
||||
default:
|
||||
return photos;
|
||||
}
|
||||
}
|
||||
|
||||
// RAGGRUPPAMENTO STILE GOOGLE PHOTOS
|
||||
// ===============================
|
||||
// 5. RAGGRUPPAMENTO
|
||||
// ===============================
|
||||
function groupByDate(photos, mode = "auto") {
|
||||
const sections = [];
|
||||
const now = new Date();
|
||||
|
|
@ -42,6 +46,7 @@ function groupByDate(photos, mode = "auto") {
|
|||
if (!date || isNaN(+date)) return "Senza data";
|
||||
|
||||
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (mode === "day") return formatDay(date);
|
||||
if (mode === "month") return formatMonth(date);
|
||||
if (mode === "year") return date.getFullYear().toString();
|
||||
|
|
@ -70,7 +75,9 @@ function groupByDate(photos, mode = "auto") {
|
|||
return sections;
|
||||
}
|
||||
|
||||
// FORMATTATORI
|
||||
// ===============================
|
||||
// 6. FORMATTATORI
|
||||
// ===============================
|
||||
function formatDay(date) {
|
||||
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" });
|
||||
}
|
||||
|
||||
// RENDER
|
||||
// ===============================
|
||||
// 7. RENDER GALLERY
|
||||
// ===============================
|
||||
function renderGallery(sections) {
|
||||
const gallery = document.getElementById("gallery");
|
||||
if (!gallery) return;
|
||||
|
|
@ -96,36 +105,11 @@ function renderGallery(sections) {
|
|||
section.photos.forEach((photo, idx) => {
|
||||
const thumbDiv = document.createElement("div");
|
||||
thumbDiv.className = "thumb";
|
||||
thumbDiv.id = "photo_" + photo.id;
|
||||
|
||||
// const th1 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub1) : photo?.thub1;
|
||||
// const th2 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub2 || photo?.thub1) : (photo?.thub2 || photo?.thub1);
|
||||
// const original = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.path) : 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);
|
||||
let th1 = photo?.thub1;
|
||||
let th2 = photo?.thub2 || photo?.thub1;
|
||||
let original = photo?.path;
|
||||
|
||||
const img = document.createElement("img");
|
||||
img.src = th1 || th2 || original || "";
|
||||
|
|
@ -133,7 +117,7 @@ console.log(th1);
|
|||
img.loading = "lazy";
|
||||
thumbDiv.appendChild(img);
|
||||
|
||||
if (photo?.mime_type && photo.mime_type.startsWith("video/")) {
|
||||
if (photo?.mime_type?.startsWith("video/")) {
|
||||
const play = document.createElement("div");
|
||||
play.className = "play-icon";
|
||||
play.textContent = "▶";
|
||||
|
|
@ -141,7 +125,6 @@ console.log(th1);
|
|||
}
|
||||
|
||||
thumbDiv.addEventListener("click", () => {
|
||||
// Chiudi sempre la strip prima di aprire una nuova foto
|
||||
window.closeBottomSheet?.();
|
||||
|
||||
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.applyFilters = applyFilters;
|
||||
window.groupByDate = groupByDate;
|
||||
window.renderGallery = renderGallery;
|
||||
window.refreshGallery = refreshGallery;
|
||||
|
|
@ -7,30 +7,21 @@ console.log("main.js avviato");
|
|||
// UTILS AUTH + SYNC HEADER UI
|
||||
// ===============================
|
||||
function isAuthenticated() {
|
||||
// Fonte verità: presenza del token in localStorage
|
||||
return !!localStorage.getItem("token");
|
||||
}
|
||||
|
||||
/**
|
||||
* Sincronizza l’UI dell’header 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() {
|
||||
const authed = isAuthenticated();
|
||||
document.body.classList.toggle('authenticated', authed);
|
||||
|
||||
const logoutBtn = document.getElementById('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';
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// PATCH: misura l'altezza reale dell'header e aggiorna --header-h
|
||||
// (serve per far partire la mappa subito sotto l’header, anche su mobile)
|
||||
// PATCH HEADER HEIGHT
|
||||
// ===============================
|
||||
(function () {
|
||||
const root = document.documentElement;
|
||||
|
|
@ -55,8 +46,7 @@ function syncHeaderAuthUI() {
|
|||
})();
|
||||
|
||||
// ===============================
|
||||
// PATCH: quando si apre la mappa (#globalMap.open) invalida le dimensioni Leaflet
|
||||
// (utile perché prima era display:none; invalidateSize evita “tagli” o tile sfasati)
|
||||
// PATCH MAP invalidateSize
|
||||
// ===============================
|
||||
(function () {
|
||||
const mapEl = document.getElementById('globalMap');
|
||||
|
|
@ -64,10 +54,8 @@ function syncHeaderAuthUI() {
|
|||
|
||||
function invalidateWhenOpen() {
|
||||
if (!mapEl.classList.contains('open')) return;
|
||||
// Aspetta un tick così il layout è aggiornato
|
||||
setTimeout(() => {
|
||||
try {
|
||||
// In mapGlobal.js imposta: window.leafletMapInstance = window.globalMap;
|
||||
window.leafletMapInstance?.invalidateSize();
|
||||
} catch (e) {
|
||||
console.warn('invalidateSize non eseguito:', e);
|
||||
|
|
@ -75,7 +63,6 @@ function syncHeaderAuthUI() {
|
|||
}, 0);
|
||||
}
|
||||
|
||||
// 1) Osserva il cambio classe (quando aggiungi .open)
|
||||
const mo = new MutationObserver((mutations) => {
|
||||
if (mutations.some(m => m.attributeName === 'class')) {
|
||||
invalidateWhenOpen();
|
||||
|
|
@ -83,14 +70,13 @@ function syncHeaderAuthUI() {
|
|||
});
|
||||
mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
// 2) Fallback: se usi il bottone #openMapBtn per aprire/chiudere
|
||||
document.getElementById('openMapBtn')?.addEventListener('click', () => {
|
||||
setTimeout(invalidateWhenOpen, 0);
|
||||
});
|
||||
})();
|
||||
|
||||
// ===============================
|
||||
// MENU ⋮ (toggle apri/chiudi con lo stesso bottone, senza global conflicts)
|
||||
// MENU ⋮
|
||||
// ===============================
|
||||
(() => {
|
||||
const optBtn = document.getElementById("optionsBtn");
|
||||
|
|
@ -103,7 +89,6 @@ function syncHeaderAuthUI() {
|
|||
try { window.closeBottomSheet?.(); } catch {}
|
||||
optSheet.classList.add("open");
|
||||
overlayEl?.classList.add("open");
|
||||
// ARIA (facoltativo)
|
||||
optBtn.setAttribute("aria-expanded", "true");
|
||||
optSheet.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
|
|
@ -111,7 +96,6 @@ function syncHeaderAuthUI() {
|
|||
function closeOptionsSheet() {
|
||||
optSheet.classList.remove("open");
|
||||
overlayEl?.classList.remove("open");
|
||||
// ARIA (facoltativo)
|
||||
optBtn.setAttribute("aria-expanded", "false");
|
||||
optSheet.setAttribute("aria-hidden", "true");
|
||||
}
|
||||
|
|
@ -123,26 +107,19 @@ function syncHeaderAuthUI() {
|
|||
else openOptionsSheet();
|
||||
}
|
||||
|
||||
// Click sul bottone: toggle (fase di cattura per battere eventuali altri handler)
|
||||
optBtn.addEventListener("click", toggleOptionsSheet, { capture: true });
|
||||
|
||||
// Chiudi clic overlay
|
||||
overlayEl?.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
closeOptionsSheet();
|
||||
});
|
||||
|
||||
// Chiudi con ESC
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && optSheet.classList.contains("open")) {
|
||||
closeOptionsSheet();
|
||||
}
|
||||
});
|
||||
|
||||
// Evita chiusure involontarie per click interni
|
||||
optSheet.addEventListener("click", (e) => e.stopPropagation());
|
||||
|
||||
// Espone una close per usarla altrove (es. dopo la scelta)
|
||||
window.closeOptionsSheet = closeOptionsSheet;
|
||||
})();
|
||||
|
||||
|
|
@ -150,41 +127,36 @@ function syncHeaderAuthUI() {
|
|||
// LOGIN AUTOMATICO SU INDEX
|
||||
// ===============================
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
// Allinea subito l’UI in base al token eventualmente già presente
|
||||
syncHeaderAuthUI();
|
||||
|
||||
try {
|
||||
// 1) Carica config
|
||||
const cfgRes = await fetch('/config');
|
||||
const cfg = await cfgRes.json();
|
||||
window.BASE_URL = cfg.baseUrl;
|
||||
|
||||
// 2) Recupera token salvato
|
||||
const savedToken = localStorage.getItem("token");
|
||||
|
||||
// Se non c'è token → mostra login
|
||||
if (!savedToken) {
|
||||
document.getElementById("loginModal").style.display = "flex";
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Verifica token
|
||||
const ping = await fetch(`${window.BASE_URL}/photos`, {
|
||||
headers: { "Authorization": "Bearer " + savedToken }
|
||||
});
|
||||
|
||||
if (!ping.ok) {
|
||||
// Token invalido → cancella e mostra login
|
||||
localStorage.removeItem("token");
|
||||
syncHeaderAuthUI(); // riallinea header subito
|
||||
syncHeaderAuthUI();
|
||||
document.getElementById("loginModal").style.display = "flex";
|
||||
return;
|
||||
}
|
||||
|
||||
// 4) Token valido → salva e carica gallery
|
||||
window.token = savedToken;
|
||||
syncHeaderAuthUI(); // <— mostra il logout senza refresh
|
||||
loadPhotos();
|
||||
syncHeaderAuthUI();
|
||||
|
||||
// 🔥 NON caricare più foto da main.js
|
||||
// La gallery viene caricata da initGallery() in gallery.js
|
||||
|
||||
} catch (err) {
|
||||
console.error("Errore autenticazione:", err);
|
||||
|
|
@ -193,62 +165,55 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|||
});
|
||||
|
||||
// ===============================
|
||||
// VARIABILI GLOBALI
|
||||
// SETTINGS (⚙️)
|
||||
// ===============================
|
||||
let currentSort = "desc";
|
||||
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 l’API 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', () => {
|
||||
document.getElementById('settingsBtn')?.addEventListener('click', () => {
|
||||
window.location.href = "admin.html";
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// LOGIN SUBMIT
|
||||
// ===============================
|
||||
/*
|
||||
document.getElementById("loginSubmit").addEventListener("click", async () => {
|
||||
const email = document.getElementById("loginEmail").value;
|
||||
const password = document.getElementById("loginPassword").value;
|
||||
const errorEl = document.getElementById("loginError");
|
||||
|
||||
errorEl.textContent = "";
|
||||
|
||||
try {
|
||||
const res = await fetch(`${window.BASE_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
errorEl.textContent = "Utente o password errati";
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const token = data.token;
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
window.token = token;
|
||||
|
||||
document.getElementById("loginModal").style.display = "none";
|
||||
|
||||
syncHeaderAuthUI();
|
||||
|
||||
// 🔥 NON caricare più foto da main.js
|
||||
// initGallery() farà tutto
|
||||
|
||||
} catch (err) {
|
||||
console.error("Errore login:", err);
|
||||
errorEl.textContent = "Errore di connessione al server";
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// ===============================
|
||||
// LOGIN SUBMIT
|
||||
// ===============================
|
||||
|
|
@ -282,9 +247,11 @@ document.getElementById("loginSubmit").addEventListener("click", async () => {
|
|||
const loginModalEl = document.getElementById("loginModal");
|
||||
if (loginModalEl) loginModalEl.style.display = "none";
|
||||
|
||||
// Riallinea UI header subito (mostra logout) e carica gallery
|
||||
// Riallinea UI header subito
|
||||
syncHeaderAuthUI();
|
||||
loadPhotos();
|
||||
|
||||
// 🔥 RICARICA LA PAGINA PER FAR PARTIRE initGallery()
|
||||
window.location.reload();
|
||||
|
||||
} catch (err) {
|
||||
console.error("Errore login:", err);
|
||||
|
|
|
|||
116
public/js/state.js
Normal 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
|
|
@ -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
|
|
@ -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;
|
||||
|
Before Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 3.9 MiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 3.8 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 4.2 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 4.5 MiB |
1
public/photos/Fabio/original/a.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
prova
|
||||
|
|
@ -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"}
|
||||
23139
public/scan.log
4
q.sh
Normal 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
|
|
@ -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 l’evento 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 dell’utente + 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
|
|
@ -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 l’utente 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||