okk
1
.Fabio.last_event
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
1774133507738
|
||||||
21
.env
|
|
@ -1,12 +1,29 @@
|
||||||
BASE_URL=https://prova.patachina.it
|
BASE_URL=https://prova.patachina.it
|
||||||
SERVER_PORT=4000
|
SERVER_PORT=4000
|
||||||
|
|
||||||
EMAIL=fabio@gmail.com
|
EMAIL=fabio@gmail.com
|
||||||
PASSWORD=master66
|
PASSWORD=master66
|
||||||
|
|
||||||
JWT_SECRET=123456789
|
JWT_SECRET=123456789
|
||||||
JWT_EXPIRES=1h
|
JWT_EXPIRES=1h
|
||||||
|
|
||||||
|
# Dove si trova la cartella public (relativa alla root del progetto)
|
||||||
|
WEB_ROOT=public
|
||||||
|
|
||||||
|
# Percorso relativo di index.json dentro public/
|
||||||
|
INDEX_PATH=photos/index.json
|
||||||
|
|
||||||
|
# true = restituisce path assoluti nei record
|
||||||
PATH_FULL=true
|
PATH_FULL=true
|
||||||
LOG_MODE=both # console | file | both
|
|
||||||
LOG_FILE=scan.log # nome file log
|
# Logging
|
||||||
|
LOG_MODE=both # console | file | both
|
||||||
|
LOG_FILE=scan.log
|
||||||
LOG_DIR=public
|
LOG_DIR=public
|
||||||
LOG_VERBOSE=true
|
LOG_VERBOSE=true
|
||||||
|
|
||||||
|
|
||||||
|
GALLERY_REFRESH_SECONDS=30
|
||||||
|
|
||||||
|
WS_PORT=4002
|
||||||
|
WS_HOST=0.0.0.0
|
||||||
|
|
|
||||||
1
Fabio.scan
Normal file
|
|
@ -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();
|
require('dotenv').config();
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
BASE_URL: process.env.BASE_URL,
|
BASE_URL: process.env.BASE_URL,
|
||||||
|
|
||||||
EMAIL: process.env.EMAIL,
|
EMAIL: process.env.EMAIL,
|
||||||
PASSWORD: process.env.PASSWORD,
|
PASSWORD: process.env.PASSWORD,
|
||||||
|
|
||||||
SEND_PHOTOS: (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true',
|
SEND_PHOTOS: (process.env.SEND_PHOTOS || 'true').toLowerCase() === 'true',
|
||||||
WRITE_INDEX: (process.env.WRITE_INDEX || 'true').toLowerCase() === 'true',
|
|
||||||
|
// Cartella public (root dei file statici)
|
||||||
WEB_ROOT: process.env.WEB_ROOT || 'public',
|
WEB_ROOT: process.env.WEB_ROOT || 'public',
|
||||||
|
|
||||||
|
// PATH_FULL ora è un boolean, non un path
|
||||||
PATH_FULL: (process.env.PATH_FULL || 'false').toLowerCase() === 'true',
|
PATH_FULL: (process.env.PATH_FULL || 'false').toLowerCase() === 'true',
|
||||||
INDEX_PATH: process.env.INDEX_PATH || path.posix.join('photos', 'index.json'),
|
|
||||||
|
// Estensioni supportate
|
||||||
SUPPORTED_EXTS: new Set([
|
SUPPORTED_EXTS: new Set([
|
||||||
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
|
'.jpg', '.jpeg', '.png', '.webp', '.heic', '.heif',
|
||||||
'.mp4', '.mov', '.m4v'
|
'.mp4', '.mov', '.m4v'
|
||||||
])
|
]),
|
||||||
|
|
||||||
|
// 🔥 NUOVO: intervallo refresh gallery (in secondi)
|
||||||
|
GALLERY_REFRESH_SECONDS: Number(process.env.GALLERY_REFRESH_SECONDS || 30)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
BIN
api_v1/database.sqlite
Normal file
|
|
@ -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,69 +1,73 @@
|
||||||
// scanner/orphanCleanup.js
|
// api_v1/scanner/orphanCleanup.js
|
||||||
const fs = require('fs');
|
const fsp = require('fs/promises');
|
||||||
const fsp = require('fs/promises');
|
const path = require('path');
|
||||||
const path = require('path');
|
|
||||||
const { WEB_ROOT, INDEX_PATH } = require('../config');
|
module.exports = function createCleanupFunctions(db) {
|
||||||
|
|
||||||
module.exports = function createCleanupFunctions(db) {
|
// 1) Recupera gli ID dal DB per una cartella specifica
|
||||||
|
async function buildIdsListForFolder(userName, cartella) {
|
||||||
async function buildIdsListForFolder(userName, cartella) {
|
try {
|
||||||
const indexPath = path.resolve(__dirname, '..', '..', WEB_ROOT, INDEX_PATH);
|
const rows = await db("photos")
|
||||||
const idsIndex = [];
|
.select("id")
|
||||||
|
.where({ user: userName, cartella });
|
||||||
if (!fs.existsSync(indexPath)) return idsIndex;
|
|
||||||
|
return rows.map(r => r.id);
|
||||||
try {
|
} catch (err) {
|
||||||
const raw = await fsp.readFile(indexPath, 'utf8');
|
console.error("Errore buildIdsListForFolder:", err);
|
||||||
const index = JSON.parse(raw);
|
return [];
|
||||||
|
}
|
||||||
const folder = index?.[userName]?.[cartella];
|
}
|
||||||
if (!folder) return idsIndex;
|
|
||||||
|
// 2) Rimuove un ID dalla lista (invariati)
|
||||||
return Object.keys(folder).filter(k => k !== "_folderHash");
|
function removeIdFromList(idsIndex, id) {
|
||||||
} catch {
|
return idsIndex.filter(x => x !== id);
|
||||||
return idsIndex;
|
}
|
||||||
}
|
|
||||||
}
|
// 3) Cancella thumbs dal filesystem usando SQLite
|
||||||
|
async function deleteThumbsById(id) {
|
||||||
function removeIdFromList(idsIndex, id) {
|
const rec = await db('photos').where({ id }).first();
|
||||||
return idsIndex.filter(x => x !== id);
|
if (!rec) return false;
|
||||||
}
|
|
||||||
|
const thumbs = [rec.thub1, rec.thub2].filter(Boolean);
|
||||||
async function deleteThumbsById(id) {
|
let deleted = false;
|
||||||
const col = db.get('photos');
|
|
||||||
const rec = col.find({ id }).value();
|
for (const t of thumbs) {
|
||||||
if (!rec) return false;
|
const abs = path.resolve(__dirname, '..', '..', 'public', t);
|
||||||
|
try {
|
||||||
const thumbs = [rec.thub1, rec.thub2].filter(Boolean);
|
await fsp.rm(abs, { force: true });
|
||||||
let deleted = false;
|
console.log(` 🔴 Thumb eliminato: ${abs}`);
|
||||||
|
deleted = true;
|
||||||
for (const t of thumbs) {
|
} catch {}
|
||||||
const abs = path.resolve(__dirname, '../public' + t);
|
}
|
||||||
if (fs.existsSync(abs)) {
|
|
||||||
await fsp.rm(abs, { force: true });
|
return deleted;
|
||||||
console.log(` 🔴 Thumb eliminato: ${abs}`);
|
}
|
||||||
deleted = true;
|
|
||||||
}
|
// 4) Cancella dal DB SQLite + registra in photo_changes
|
||||||
}
|
async function deleteFromDB(id, userName) {
|
||||||
return deleted;
|
const deleted = await db('photos').where({ id }).del();
|
||||||
}
|
|
||||||
|
if (deleted > 0) {
|
||||||
function deleteFromDB(id) {
|
console.log(` 🔴 Eliminato dal DB: ${id}`);
|
||||||
const col = db.get('photos');
|
|
||||||
const exists = col.find({ id }).value();
|
// 🔥 REGISTRAZIONE IN photo_changes
|
||||||
|
await db('photo_changes').insert({
|
||||||
if (exists) {
|
photo_id: id,
|
||||||
col.remove({ id }).write();
|
user: userName,
|
||||||
console.log(` 🔴 Eliminato dal DB: ${id}`);
|
change_type: 'removed',
|
||||||
return true;
|
timestamp: new Date().toISOString()
|
||||||
}
|
});
|
||||||
return false;
|
|
||||||
}
|
return true;
|
||||||
|
}
|
||||||
return {
|
|
||||||
buildIdsListForFolder,
|
return false;
|
||||||
removeIdFromList,
|
}
|
||||||
deleteThumbsById,
|
|
||||||
deleteFromDB
|
return {
|
||||||
};
|
buildIdsListForFolder,
|
||||||
};
|
removeIdFromList,
|
||||||
|
deleteThumbsById,
|
||||||
|
deleteFromDB
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
@ -1,60 +1,24 @@
|
||||||
const axios = require('axios');
|
// scanner/postWithAuth.js
|
||||||
const { BASE_URL, EMAIL, PASSWORD, SEND_PHOTOS } = require('../config');
|
// Versione locale: niente HTTP, niente token, solo DB
|
||||||
|
|
||||||
let cachedToken = null;
|
module.exports = function createPostToDB(db) {
|
||||||
|
|
||||||
async function getToken(force = false) {
|
/**
|
||||||
if (!SEND_PHOTOS) return null;
|
* Inserisce o aggiorna un record nel DB SQLite
|
||||||
if (cachedToken && !force) return cachedToken;
|
* (sostituisce completamente axios.post)
|
||||||
|
*/
|
||||||
try {
|
async function postToDB(record) {
|
||||||
const res = await axios.post(`${BASE_URL}/auth/login`, {
|
if (!record || !record.id) {
|
||||||
email: EMAIL,
|
throw new Error("Record non valido");
|
||||||
password: PASSWORD
|
}
|
||||||
});
|
|
||||||
|
await db('photos')
|
||||||
cachedToken = res.data.token;
|
.insert(record)
|
||||||
return cachedToken;
|
.onConflict('id')
|
||||||
|
.merge();
|
||||||
} catch (err) {
|
|
||||||
console.error('ERRORE LOGIN:', err.message);
|
return true;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
}
|
return postToDB;
|
||||||
|
};
|
||||||
async function postWithAuth(url, payload) {
|
|
||||||
if (!SEND_PHOTOS) return;
|
|
||||||
|
|
||||||
let token = await getToken();
|
|
||||||
if (!token) throw new Error('Token assente');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await axios.post(url, payload, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: 20000,
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
if (err.response && err.response.status === 401) {
|
|
||||||
token = await getToken(true);
|
|
||||||
if (!token) throw err;
|
|
||||||
|
|
||||||
await axios.post(url, payload, {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
timeout: 20000,
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = postWithAuth;
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,70 +1,64 @@
|
||||||
// scanner/scanCartella.js
|
// api_v1/scanner/scanCartella.js
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fsp = require('fs/promises');
|
const fsp = require('fs/promises');
|
||||||
const processFile = require('./processFile');
|
const { sha256 } = require('./utils');
|
||||||
const { sha256 } = require('./utils');
|
const { SUPPORTED_EXTS } = require('../config');
|
||||||
const { SUPPORTED_EXTS } = require('../config');
|
|
||||||
|
/**
|
||||||
async function* scanCartella(userName, cartella, absCartella, previousIndexTree) {
|
* scanCartella: genera informazioni sui file nella cartella
|
||||||
|
* Non esegue processFile né scrive sul DB: lascia la logica di confronto a scanPhoto.
|
||||||
async function* walk(currentAbs, relPath = '') {
|
*
|
||||||
let entries = [];
|
* Parametri:
|
||||||
try {
|
* - userName
|
||||||
entries = await fsp.readdir(currentAbs, { withFileTypes: true });
|
* - cartella
|
||||||
} catch {
|
* - absCartella
|
||||||
return;
|
* - db (opzionale, mantenuto per compatibilità)
|
||||||
}
|
*/
|
||||||
|
async function* scanCartella(userName, cartella, absCartella, db) {
|
||||||
for (const e of entries) {
|
|
||||||
const absPath = path.join(currentAbs, e.name);
|
async function* walk(currentAbs, relPath = '') {
|
||||||
|
let entries = [];
|
||||||
if (e.isDirectory()) {
|
try {
|
||||||
yield* walk(absPath, path.join(relPath, e.name));
|
entries = await fsp.readdir(currentAbs, { withFileTypes: true });
|
||||||
continue;
|
} catch {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
const ext = path.extname(e.name).toLowerCase();
|
|
||||||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
for (const e of entries) {
|
||||||
|
const absPath = path.join(currentAbs, e.name);
|
||||||
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
|
|
||||||
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
if (e.isDirectory()) {
|
||||||
|
yield* walk(absPath, path.join(relPath, e.name));
|
||||||
let st;
|
continue;
|
||||||
try { st = await fsp.stat(absPath); } catch { continue; }
|
}
|
||||||
|
|
||||||
const hash = sha256(`${st.size}-${st.mtimeMs}`);
|
const ext = path.extname(e.name).toLowerCase();
|
||||||
const prev = previousIndexTree?.[userName]?.[cartella]?.[id];
|
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||||
|
|
||||||
if (prev && prev.hash === hash) {
|
const fileRelPath = relPath ? `${relPath}/${e.name}` : e.name;
|
||||||
yield {
|
|
||||||
id,
|
// ID deterministico (stesso criterio di prima)
|
||||||
user: userName,
|
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
||||||
cartella,
|
|
||||||
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`,
|
// Stat veloce
|
||||||
_indexHash: hash,
|
let st;
|
||||||
unchanged: true
|
try { st = await fsp.stat(absPath); } catch { continue; }
|
||||||
};
|
|
||||||
continue;
|
yield {
|
||||||
}
|
id,
|
||||||
|
user: userName,
|
||||||
const meta = await processFile(
|
cartella,
|
||||||
userName,
|
name: e.name,
|
||||||
cartella,
|
relPath: fileRelPath,
|
||||||
fileRelPath,
|
absPath,
|
||||||
absPath,
|
ext,
|
||||||
ext,
|
stat: st,
|
||||||
st
|
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`
|
||||||
);
|
};
|
||||||
|
}
|
||||||
meta.id = id;
|
}
|
||||||
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
|
|
||||||
meta._indexHash = hash;
|
yield* walk(absCartella);
|
||||||
|
}
|
||||||
yield meta;
|
|
||||||
}
|
module.exports = scanCartella;
|
||||||
}
|
|
||||||
|
|
||||||
yield* walk(absCartella);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = scanCartella;
|
|
||||||
|
|
@ -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,247 +1,343 @@
|
||||||
// scanner/scanPhoto.js
|
// api_v1/scanner/scanPhoto.js
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const fsp = require('fs/promises');
|
const fsp = require('fs/promises');
|
||||||
|
|
||||||
const { log } = require('./logger');
|
const { log } = require('./logger');
|
||||||
const { loadPreviousIndex, saveIndex } = require('./indexStore');
|
const scanCartella = require('./scanCartella');
|
||||||
const scanCartella = require('./scanCartella');
|
const processFile = require('./processFile');
|
||||||
const postWithAuth = require('./postWithAuth');
|
const postWithAuth = require('./postWithAuth');
|
||||||
|
const { sha256 } = require('./utils');
|
||||||
const {
|
|
||||||
WEB_ROOT,
|
const {
|
||||||
SEND_PHOTOS,
|
WEB_ROOT,
|
||||||
BASE_URL,
|
SEND_PHOTOS,
|
||||||
WRITE_INDEX,
|
BASE_URL,
|
||||||
SUPPORTED_EXTS
|
SUPPORTED_EXTS
|
||||||
} = require('../config');
|
} = require('../config');
|
||||||
|
|
||||||
const createCleanupFunctions = require('./orphanCleanup');
|
const createCleanupFunctions = require('./orphanCleanup');
|
||||||
|
|
||||||
// Variabile per log completo o ridotto
|
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
|
||||||
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
|
|
||||||
|
function formatTime(ms) {
|
||||||
// Formatta ms → HH:MM:SS
|
const sec = Math.floor(ms / 1000);
|
||||||
function formatTime(ms) {
|
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
||||||
const sec = Math.floor(ms / 1000);
|
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');
|
||||||
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
const s = String(sec % 60).padStart(2, '0');
|
||||||
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');
|
return `${h}:${m}:${s}`;
|
||||||
const s = String(sec % 60).padStart(2, '0');
|
}
|
||||||
return `${h}:${m}:${s}`;
|
|
||||||
}
|
async function countFilesFast(rootDir) {
|
||||||
|
let count = 0;
|
||||||
// ---------------------------------------------------------
|
|
||||||
// ⚡ CONTEGGIO FILE VELOCE (senza hashing, EXIF, sharp, ecc.)
|
async function walk(dir) {
|
||||||
// ---------------------------------------------------------
|
let entries = [];
|
||||||
async function countFilesFast(rootDir) {
|
try {
|
||||||
let count = 0;
|
entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
async function walk(dir) {
|
return;
|
||||||
let entries = [];
|
}
|
||||||
try {
|
|
||||||
entries = await fsp.readdir(dir, { withFileTypes: true });
|
for (const e of entries) {
|
||||||
} catch {
|
const abs = path.join(dir, e.name);
|
||||||
return;
|
|
||||||
}
|
if (e.isDirectory()) {
|
||||||
|
await walk(abs);
|
||||||
for (const e of entries) {
|
} else {
|
||||||
const abs = path.join(dir, e.name);
|
const ext = path.extname(e.name).toLowerCase();
|
||||||
|
if (SUPPORTED_EXTS.has(ext)) {
|
||||||
if (e.isDirectory()) {
|
count++;
|
||||||
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) {
|
||||||
await walk(rootDir);
|
const start = Date.now();
|
||||||
return count;
|
log(`🔵 Inizio scan globale per user=${userName}`);
|
||||||
}
|
|
||||||
|
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||||
async function scanPhoto(dir, userName, db) {
|
|
||||||
const start = Date.now();
|
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||||
log(`🔵 Inizio scan globale per user=${userName}`);
|
const userDir = path.join(photosRoot, userName, 'original');
|
||||||
|
|
||||||
const previousIndexTree = await loadPreviousIndex();
|
let newFiles = [];
|
||||||
const nextIndexTree = JSON.parse(JSON.stringify(previousIndexTree || {}));
|
|
||||||
|
const TOTAL_FILES = await countFilesFast(userDir);
|
||||||
const {
|
if (TOTAL_FILES === 0) {
|
||||||
buildIdsListForFolder,
|
log(`Nessun file trovato per user=${userName}`);
|
||||||
removeIdFromList,
|
return [];
|
||||||
deleteThumbsById,
|
}
|
||||||
deleteFromDB
|
|
||||||
} = createCleanupFunctions(db);
|
log(`📦 File totali da processare: ${TOTAL_FILES}`);
|
||||||
|
|
||||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
let CURRENT = 0;
|
||||||
const userDir = path.join(photosRoot, userName, 'original');
|
|
||||||
|
async function updateStatusFile() {
|
||||||
let totalNew = 0;
|
const now = Date.now();
|
||||||
let totalDeleted = 0;
|
const elapsedMs = now - start;
|
||||||
let totalUnchanged = 0;
|
const avg = elapsedMs / Math.max(CURRENT, 1);
|
||||||
|
const remainingMs = (TOTAL_FILES - CURRENT) * avg;
|
||||||
let newFiles = [];
|
|
||||||
|
const status = {
|
||||||
// ---------------------------------------------------------
|
current: CURRENT,
|
||||||
// 1) ⚡ CONTEGGIO FILE SUPER VELOCE
|
total: TOTAL_FILES,
|
||||||
// ---------------------------------------------------------
|
percent: Number((CURRENT / TOTAL_FILES * 100).toFixed(2)),
|
||||||
const TOTAL_FILES = await countFilesFast(userDir);
|
eta: formatTime(remainingMs),
|
||||||
|
elapsed: formatTime(elapsedMs)
|
||||||
if (TOTAL_FILES === 0) {
|
};
|
||||||
log(`Nessun file trovato per user=${userName}`);
|
|
||||||
return [];
|
const statusPath = path.resolve(__dirname, "..", "..", "public/photos/scan_status.json");
|
||||||
}
|
await fsp.writeFile(statusPath, JSON.stringify(status));
|
||||||
|
}
|
||||||
log(`📦 File totali da processare: ${TOTAL_FILES}`);
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
let CURRENT = 0;
|
// SCAN DI UNA SINGOLA CARTELLA
|
||||||
|
// ---------------------------------------------------------
|
||||||
// ---------------------------------------------------------
|
async function scanSingleFolder(user, cartella, absCartella) {
|
||||||
// Funzione per aggiornare il file statico leggibile dall’HTML
|
log(`📁 Inizio cartella: ${cartella}`);
|
||||||
// ---------------------------------------------------------
|
|
||||||
async function updateStatusFile() {
|
const rows = await db('photos')
|
||||||
const now = Date.now();
|
.where({ user, cartella })
|
||||||
const elapsedMs = now - start;
|
.select('id');
|
||||||
const avg = elapsedMs / CURRENT;
|
|
||||||
const remainingMs = (TOTAL_FILES - CURRENT) * avg;
|
const idsSet = new Set(rows.map(r => r.id));
|
||||||
|
|
||||||
const status = {
|
for await (const f of scanCartella(user, cartella, absCartella, db)) {
|
||||||
current: CURRENT,
|
CURRENT++;
|
||||||
total: TOTAL_FILES,
|
await updateStatusFile();
|
||||||
percent: Number((CURRENT / TOTAL_FILES * 100).toFixed(2)),
|
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
|
||||||
eta: formatTime(remainingMs),
|
const fileName = f.name;
|
||||||
elapsed: formatTime(elapsedMs)
|
const id = f.id;
|
||||||
};
|
const st = f.stat;
|
||||||
|
|
||||||
const statusPath = path.resolve(__dirname, "..", "..", "public/photos/scan_status.json");
|
|
||||||
await fsp.writeFile(statusPath, JSON.stringify(status));
|
let prev = await db("photos")
|
||||||
}
|
.select("id", "size_bytes", "mtimeMs", "_indexHash", "fast_hash", "path")
|
||||||
|
.where({ id })
|
||||||
// ---------------------------------------------------------
|
.first();
|
||||||
// 2) SCAN REALE DELLE CARTELLE
|
|
||||||
// ---------------------------------------------------------
|
if (prev && LOG_VERBOSE) {
|
||||||
async function scanSingleFolder(user, cartella, absCartella) {
|
log(`${prefix} ⚪ Invariato: ${fileName}`);
|
||||||
log(`📁 Inizio cartella: ${cartella}`);
|
}
|
||||||
|
|
||||||
let idsIndex = await buildIdsListForFolder(user, cartella);
|
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||||
|
|
||||||
let newCount = 0;
|
// ---------------------------------------------------------
|
||||||
let unchangedCount = 0;
|
// PATH CAMBIATO = NUOVO FILE
|
||||||
let deletedCount = 0;
|
// ---------------------------------------------------------
|
||||||
|
if (prev && prev.path !== f.path) {
|
||||||
for await (const m of scanCartella(user, cartella, absCartella, previousIndexTree)) {
|
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||||
CURRENT++;
|
prev = null;
|
||||||
const fileName = m.path.split('/').pop();
|
}
|
||||||
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
// Aggiorna file statico per l’HTML
|
// NUOVO FILE
|
||||||
await updateStatusFile();
|
// ---------------------------------------------------------
|
||||||
|
if (!prev) {
|
||||||
// ⚪ FILE INVARIATO
|
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||||
if (m.unchanged) {
|
const meta = await processFile(
|
||||||
if (LOG_VERBOSE) {
|
user,
|
||||||
log(`${prefix} ⚪ Invariato: ${fileName}`);
|
cartella,
|
||||||
}
|
f.relPath,
|
||||||
idsIndex = removeIdFromList(idsIndex, m.id);
|
f.absPath,
|
||||||
unchangedCount++;
|
f.ext,
|
||||||
continue;
|
st
|
||||||
}
|
);
|
||||||
|
|
||||||
// 🟢 FILE NUOVO O MODIFICATO
|
meta.id = id;
|
||||||
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
meta.path = f.path;
|
||||||
|
|
||||||
nextIndexTree[m.user] ??= {};
|
// INSERT CORRETTO (senza colonna gps)
|
||||||
nextIndexTree[m.user][m.cartella] ??= {};
|
await db("photos").insert({
|
||||||
nextIndexTree[m.user][m.cartella][m.id] = {
|
id: meta.id,
|
||||||
id: m.id,
|
user,
|
||||||
user: m.user,
|
cartella,
|
||||||
cartella: m.cartella,
|
name: meta.name,
|
||||||
path: m.path,
|
path: meta.path,
|
||||||
hash: m._indexHash
|
thub1: meta.thub1,
|
||||||
};
|
thub2: meta.thub2,
|
||||||
|
mime_type: meta.mime_type,
|
||||||
idsIndex = removeIdFromList(idsIndex, m.id);
|
width: meta.width,
|
||||||
newCount++;
|
height: meta.height,
|
||||||
newFiles.push(m);
|
rotation: meta.rotation,
|
||||||
}
|
size_bytes: meta.size_bytes,
|
||||||
|
mtimeMs: meta.mtimeMs,
|
||||||
// 🔴 ORFANI
|
duration_ms: meta.duration_ms,
|
||||||
for (const orphanId of idsIndex) {
|
taken_at: meta.taken_at,
|
||||||
const old = previousIndexTree?.[user]?.[cartella]?.[orphanId];
|
data: meta.data,
|
||||||
const fileName = old?.path?.split('/').pop() || orphanId;
|
lat: meta.gps?.lat ?? null,
|
||||||
|
lon: meta.gps?.lng ?? null,
|
||||||
log(` 🔴 Eliminato: ${fileName}`);
|
alt: meta.gps?.alt ?? null,
|
||||||
|
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||||
await deleteThumbsById(orphanId);
|
_indexHash: meta._indexHash,
|
||||||
deleteFromDB(orphanId);
|
fast_hash: fastHash
|
||||||
|
});
|
||||||
const userTree = nextIndexTree[user];
|
|
||||||
if (userTree?.[cartella]?.[orphanId]) {
|
await db('photo_changes').insert({
|
||||||
delete userTree[cartella][orphanId];
|
photo_id: meta.id,
|
||||||
}
|
user,
|
||||||
|
change_type: 'added',
|
||||||
deletedCount++;
|
timestamp: new Date().toISOString()
|
||||||
}
|
});
|
||||||
|
|
||||||
log(`📊 Fine cartella ${cartella}: invariati=${unchangedCount}, nuovi=${newCount}, cancellati=${deletedCount}`);
|
idsSet.delete(id);
|
||||||
|
|
||||||
totalNew += newCount;
|
newFiles.push(meta);
|
||||||
totalDeleted += deletedCount;
|
//log(`🟢 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
|
||||||
totalUnchanged += unchangedCount;
|
|
||||||
}
|
continue;
|
||||||
|
}
|
||||||
// ---------------------------------------------------------
|
|
||||||
// SCAN SOTTOCARTELLE
|
// ---------------------------------------------------------
|
||||||
// ---------------------------------------------------------
|
// FAST-SIZE-SKIP
|
||||||
let entries = [];
|
// ---------------------------------------------------------
|
||||||
try {
|
if (prev.size_bytes === st.size) {
|
||||||
entries = await fsp.readdir(userDir, { withFileTypes: true });
|
//log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
|
||||||
} catch {
|
idsSet.delete(id);
|
||||||
log(`Nessuna directory per utente ${userName}`);
|
|
||||||
return [];
|
await db("photos")
|
||||||
}
|
.where({ id })
|
||||||
|
.update({
|
||||||
for (const e of entries) {
|
path: f.path,
|
||||||
if (!e.isDirectory()) continue;
|
size_bytes: st.size,
|
||||||
const cartella = e.name;
|
mtimeMs: st.mtimeMs,
|
||||||
const absCartella = path.join(userDir, cartella);
|
fast_hash: fastHash
|
||||||
await scanSingleFolder(userName, cartella, absCartella);
|
});
|
||||||
}
|
|
||||||
|
continue;
|
||||||
// ---------------------------------------------------------
|
}
|
||||||
// SALVO INDEX
|
|
||||||
// ---------------------------------------------------------
|
// ---------------------------------------------------------
|
||||||
if (WRITE_INDEX) {
|
// FAST-HASH-SKIP
|
||||||
await saveIndex(nextIndexTree);
|
// ---------------------------------------------------------
|
||||||
}
|
if (prev.fast_hash === fastHash) {
|
||||||
|
//log(`🔵 [FAST-HASH-SKIP] id=${id}`);
|
||||||
// ---------------------------------------------------------
|
idsSet.delete(id);
|
||||||
// INVIO AL SERVER / POPOLAZIONE DB
|
|
||||||
// ---------------------------------------------------------
|
await db("photos")
|
||||||
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
|
.where({ id })
|
||||||
for (const p of newFiles) {
|
.update({
|
||||||
const fileName = p.path.split('/').pop();
|
path: f.path,
|
||||||
|
size_bytes: st.size,
|
||||||
try {
|
mtimeMs: st.mtimeMs
|
||||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
});
|
||||||
log(`📤 Inviato al server: ${fileName}`);
|
|
||||||
} catch (err) {
|
continue;
|
||||||
log(`Errore invio ${fileName}: ${err.message}`);
|
}
|
||||||
}
|
|
||||||
}
|
// ---------------------------------------------------------
|
||||||
}
|
// MODIFICATO
|
||||||
|
// ---------------------------------------------------------
|
||||||
// ---------------------------------------------------------
|
//log(`🟠 [FULL-SCAN] id=${id}`);
|
||||||
// FINE SCAN
|
log(`${prefix} 🟠 Nuovo/Modificato: ${fileName}`);
|
||||||
// ---------------------------------------------------------
|
const meta = await processFile(
|
||||||
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
user,
|
||||||
|
cartella,
|
||||||
log(`🟣 Scan COMPLETATO: nuovi=${totalNew}, cancellati=${totalDeleted}, invariati=${totalUnchanged}`);
|
f.relPath,
|
||||||
log(`⏱ Tempo totale: ${elapsed}s`);
|
f.absPath,
|
||||||
|
f.ext,
|
||||||
return newFiles;
|
st
|
||||||
}
|
);
|
||||||
|
|
||||||
module.exports = scanPhoto;
|
meta.id = id;
|
||||||
|
meta.path = f.path;
|
||||||
|
|
||||||
|
await db("photos")
|
||||||
|
.insert({
|
||||||
|
id: meta.id,
|
||||||
|
user,
|
||||||
|
cartella,
|
||||||
|
name: meta.name,
|
||||||
|
path: meta.path,
|
||||||
|
thub1: meta.thub1,
|
||||||
|
thub2: meta.thub2,
|
||||||
|
mime_type: meta.mime_type,
|
||||||
|
width: meta.width,
|
||||||
|
height: meta.height,
|
||||||
|
rotation: meta.rotation,
|
||||||
|
size_bytes: meta.size_bytes,
|
||||||
|
mtimeMs: meta.mtimeMs,
|
||||||
|
duration_ms: meta.duration_ms,
|
||||||
|
taken_at: meta.taken_at,
|
||||||
|
data: meta.data,
|
||||||
|
lat: meta.gps?.lat ?? null,
|
||||||
|
lon: meta.gps?.lng ?? null,
|
||||||
|
alt: meta.gps?.alt ?? null,
|
||||||
|
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||||
|
_indexHash: meta._indexHash,
|
||||||
|
fast_hash: fastHash
|
||||||
|
})
|
||||||
|
.onConflict("id")
|
||||||
|
.merge();
|
||||||
|
|
||||||
|
await db('photo_changes').insert({
|
||||||
|
photo_id: meta.id,
|
||||||
|
user,
|
||||||
|
change_type: 'updated',
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
idsSet.delete(id);
|
||||||
|
newFiles.push(meta);
|
||||||
|
log(`${prefix} 🟠 Nuovo/Modificato al server ${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// ORFANI
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
for (const orphanId of idsSet) {
|
||||||
|
log(` 🔴 Cancellato ${orphanId}`);
|
||||||
|
await deleteThumbsById(orphanId);
|
||||||
|
await deleteFromDB(orphanId, user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// SCAN DI TUTTE LE CARTELLE
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
let entries = [];
|
||||||
|
try {
|
||||||
|
entries = await fsp.readdir(userDir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
log(`Nessuna directory per utente ${userName}`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const e of entries) {
|
||||||
|
if (!e.isDirectory()) continue;
|
||||||
|
const cartella = e.name;
|
||||||
|
const absCartella = path.join(userDir, cartella);
|
||||||
|
await scanSingleFolder(userName, cartella, absCartella);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
// INVIO AL SERVER REMOTO
|
||||||
|
// ---------------------------------------------------------
|
||||||
|
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
|
||||||
|
log(`📤 [SEND START] newFiles=${newFiles.length}`);
|
||||||
|
|
||||||
|
for (const p of newFiles) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||||
|
log(`📥 [SENT TO SERVER] ${p.name}`);
|
||||||
|
} catch (err) {
|
||||||
|
log(`❌ [SERVER SENT ERROR] ${p.name}→ ${err.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(`⚠️ [NO SEND] newFiles=${newFiles.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
||||||
|
log(`🟣 Scan COMPLETATO in ${elapsed}s`);
|
||||||
|
|
||||||
|
return newFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = scanPhoto;
|
||||||
|
|
|
||||||
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
|
||||||
1816
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": {
|
"dependencies": {
|
||||||
"async": "^3.2.6",
|
"axios": "^1.13.6",
|
||||||
"axios": "^1.13.5",
|
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
"body-parser": "^2.2.2",
|
"better-sqlite3": "^12.8.0",
|
||||||
"dotenv": "^17.3.1",
|
"dotenv": "^17.3.1",
|
||||||
"exifreader": "^4.36.2",
|
"exifreader": "^4.37.0",
|
||||||
"json-server": "^0.17.4",
|
"express": "^5.2.1",
|
||||||
"jsonwebtoken": "^9.0.3",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"path": "^0.12.7",
|
"knex": "^3.1.0",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5",
|
||||||
|
"ws": "^8.19.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,15 @@
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#changesBox {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f7f7f7;
|
||||||
|
width: 350px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
@ -40,8 +49,19 @@
|
||||||
<button onclick="deletePhoto()">Cancella Foto per ID</button>
|
<button onclick="deletePhoto()">Cancella Foto per ID</button>
|
||||||
<button onclick="findIdIndex()">Cerca ID in index.json</button>
|
<button onclick="findIdIndex()">Cerca ID in index.json</button>
|
||||||
<button onclick="resetDBuser()">Reset DB Utente</button>
|
<button onclick="resetDBuser()">Reset DB Utente</button>
|
||||||
|
<button onclick="searchPhotoById()">Cerca Foto (nuovo /byIds)</button>
|
||||||
<button onclick="window.location.href='index.html'">Torna alla galleria</button>
|
<button onclick="window.location.href='index.html'">Torna alla galleria</button>
|
||||||
|
|
||||||
|
<!-- 🔥 NUOVO BLOCCO: /photos/changes -->
|
||||||
|
<div id="changesBox">
|
||||||
|
<h4>Controlla /photos/changes</h4>
|
||||||
|
|
||||||
|
<label>Since (data/ora):</label><br>
|
||||||
|
<input type="datetime-local" id="sinceInput" style="width: 100%; margin-top:5px;"><br><br>
|
||||||
|
|
||||||
|
<button onclick="showChanges()">Mostra cambiamenti</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Barra di avanzamento -->
|
<!-- Barra di avanzamento -->
|
||||||
<div id="progressContainer">
|
<div id="progressContainer">
|
||||||
<div id="progressBar"></div>
|
<div id="progressBar"></div>
|
||||||
|
|
@ -76,6 +96,30 @@ if (!token) {
|
||||||
// FUNZIONI ESISTENTI
|
// FUNZIONI ESISTENTI
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
||||||
|
async function searchPhotoById() {
|
||||||
|
const id = prompt("Inserisci l'ID della foto da cercare:");
|
||||||
|
if (!id) return;
|
||||||
|
|
||||||
|
const payload = parseJwt(token);
|
||||||
|
const user = payload.name;
|
||||||
|
|
||||||
|
const url = `${BASE_URL}/photos/byIds?id=${encodeURIComponent(id)}&user=${encodeURIComponent(user)}`;
|
||||||
|
|
||||||
|
console.log("🔍 [searchPhotoById] URL chiamato:", url);
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { "Authorization": "Bearer " + token }
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = await res.json();
|
||||||
|
|
||||||
|
console.log("🔍 [searchPhotoById] Risposta:", out);
|
||||||
|
|
||||||
|
document.getElementById("out").textContent =
|
||||||
|
JSON.stringify(out, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function deletePhoto() {
|
async function deletePhoto() {
|
||||||
const id = prompt("Inserisci l'ID della foto da cancellare:");
|
const id = prompt("Inserisci l'ID della foto da cancellare:");
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
|
|
@ -153,18 +197,45 @@ async function resetDB() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------
|
// -------------------------
|
||||||
// NUOVA PARTE: SCAN + BARRA
|
// NUOVA FUNZIONE: /photos/changes
|
||||||
|
// -------------------------
|
||||||
|
|
||||||
|
async function showChanges() {
|
||||||
|
const sinceInput = document.getElementById("sinceInput").value;
|
||||||
|
|
||||||
|
if (!sinceInput) {
|
||||||
|
alert("Seleziona una data/ora valida.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertiamo datetime-local → ISO
|
||||||
|
const sinceISO = new Date(sinceInput).toISOString();
|
||||||
|
|
||||||
|
const payload = parseJwt(token);
|
||||||
|
const user = payload.name;
|
||||||
|
|
||||||
|
const url = `${BASE_URL}/photos/changes?since=${encodeURIComponent(sinceISO)}&user=${encodeURIComponent(user)}`;
|
||||||
|
|
||||||
|
const res = await fetch(url, {
|
||||||
|
headers: { "Authorization": "Bearer " + token }
|
||||||
|
});
|
||||||
|
|
||||||
|
const out = await res.json();
|
||||||
|
document.getElementById("out").textContent =
|
||||||
|
JSON.stringify(out, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------
|
||||||
|
// SCAN + BARRA
|
||||||
// -------------------------
|
// -------------------------
|
||||||
|
|
||||||
let scanInterval = null;
|
let scanInterval = null;
|
||||||
|
|
||||||
async function scan() {
|
async function scan() {
|
||||||
// Avvia lo scan sul backend
|
|
||||||
fetch(`${BASE_URL}/scan`, {
|
fetch(`${BASE_URL}/scan`, {
|
||||||
headers: { "Authorization": "Bearer " + token }
|
headers: { "Authorization": "Bearer " + token }
|
||||||
});
|
});
|
||||||
|
|
||||||
// Avvia il polling dello stato
|
|
||||||
startScanStatusPolling();
|
startScanStatusPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,23 +244,19 @@ async function startScanStatusPolling() {
|
||||||
|
|
||||||
scanInterval = setInterval(async () => {
|
scanInterval = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
// PERCORSO CORRETTO
|
|
||||||
const res = await fetch(`/photos/scan_status.json?ts=` + Date.now());
|
const res = await fetch(`/photos/scan_status.json?ts=` + Date.now());
|
||||||
if (!res.ok) return;
|
if (!res.ok) return;
|
||||||
|
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
|
||||||
// Aggiorna testo
|
|
||||||
document.getElementById("scanProgress").textContent =
|
document.getElementById("scanProgress").textContent =
|
||||||
`Progresso: ${data.current}/${data.total} (${data.percent}%)`;
|
`Progresso: ${data.current}/${data.total} (${data.percent}%)`;
|
||||||
|
|
||||||
document.getElementById("scanEta").textContent =
|
document.getElementById("scanEta").textContent =
|
||||||
`Tempo stimato rimanente: ${data.eta}`;
|
`Tempo stimato rimanente: ${data.eta}`;
|
||||||
|
|
||||||
// Aggiorna barra
|
|
||||||
document.getElementById("progressBar").style.width = data.percent + "%";
|
document.getElementById("progressBar").style.width = data.percent + "%";
|
||||||
|
|
||||||
// Fine scan
|
|
||||||
if (data.current >= data.total && data.total > 0) {
|
if (data.current >= data.total && data.total > 0) {
|
||||||
clearInterval(scanInterval);
|
clearInterval(scanInterval);
|
||||||
scanInterval = null;
|
scanInterval = null;
|
||||||
|
|
@ -208,4 +275,4 @@ window.onload = async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -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"
|
title="Logout"
|
||||||
aria-label="Logout">
|
aria-label="Logout">
|
||||||
|
|
||||||
<!-- Icona PNG -->
|
|
||||||
<img
|
<img
|
||||||
class="logout-icon"
|
class="logout-icon"
|
||||||
src="img/switch.png"
|
src="img/switch.png"
|
||||||
|
|
@ -70,7 +69,6 @@
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-close" id="modalClose">×</div>
|
<div class="modal-close" id="modalClose">×</div>
|
||||||
|
|
||||||
<!-- Frecce navigazione -->
|
|
||||||
<button class="modal-nav-btn prev" id="modalPrev" type="button" aria-label="Precedente"><</button>
|
<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>
|
<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>
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
|
||||||
<!-- MarkerCluster JS -->
|
<!-- MarkerCluster JS -->
|
||||||
<script src="https://unpkg.com/leaflet.markercluster/dist/leaflet.markercluster.js"></script>
|
<script src="https://unpkg.com/leaflet.markercluster/dist/MarkerCluster.js"></script>
|
||||||
|
|
||||||
<!-- Eruda Debug Console -->
|
<!-- Eruda Debug Console -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
<script src="https://cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
<script>
|
<script>
|
||||||
eruda.init();
|
eruda.init();
|
||||||
console.log("Eruda inizializzato");
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Debug immediato -->
|
<!-- Debug immediato -->
|
||||||
|
|
@ -163,13 +160,17 @@
|
||||||
<script src="js/gallery.js"></script>
|
<script src="js/gallery.js"></script>
|
||||||
<script src="js/modal.js"></script>
|
<script src="js/modal.js"></script>
|
||||||
<script src="js/infoPanel.js"></script>
|
<script src="js/infoPanel.js"></script>
|
||||||
|
|
||||||
<!-- DEVE ESSERE PRIMA DI mapGlobal.js -->
|
|
||||||
<script src="js/bottomSheet.js"></script>
|
<script src="js/bottomSheet.js"></script>
|
||||||
|
|
||||||
<script src="js/mapGlobal.js"></script>
|
<script src="js/mapGlobal.js"></script>
|
||||||
<script src="js/logout.js"></script>
|
<script src="js/logout.js"></script>
|
||||||
|
|
||||||
|
<!-- 🔥 NUOVI FILE PER SYNC INCREMENTALE -->
|
||||||
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/state.js"></script>
|
||||||
|
<script src="js/sync.js"></script>
|
||||||
|
|
||||||
|
<!-- MAIN -->
|
||||||
<script src="js/main.js"></script>
|
<script src="js/main.js"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
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,165 +1,194 @@
|
||||||
// ===============================
|
// ===============================
|
||||||
// GALLERY — completa, stile Google Photos
|
// GALLERY — con SYNC INCREMENTALE + REFRESH DA .ENV
|
||||||
// - Ordinamento
|
// ===============================
|
||||||
// - Filtri
|
|
||||||
// - Raggruppamento (auto/giorno/mese/anno)
|
let currentUser = null;
|
||||||
// - Render a sezioni
|
let refreshSeconds = 30;
|
||||||
// - Click: openModalFromList(sezione, indice) se disponibile (fallback openModal)
|
|
||||||
// ===============================
|
// ===============================
|
||||||
|
// 3. ORDINAMENTO
|
||||||
// ORDINAMENTO
|
// ===============================
|
||||||
function sortByDate(photos, direction = "desc") {
|
function sortByDate(photos, direction = "desc") {
|
||||||
return photos.slice().sort((a, b) => {
|
return photos.slice().sort((a, b) => {
|
||||||
const da = a?.taken_at ? new Date(a.taken_at) : 0;
|
const da = a?.taken_at ? new Date(a.taken_at) : 0;
|
||||||
const db = b?.taken_at ? new Date(b.taken_at) : 0;
|
const db = b?.taken_at ? new Date(b.taken_at) : 0;
|
||||||
return direction === "asc" ? (da - db) : (db - da);
|
return direction === "asc" ? (da - db) : (db - da);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// FILTRI
|
// ===============================
|
||||||
function applyFilters(photos) {
|
// 4. FILTRI
|
||||||
if (!window.currentFilter) return photos;
|
// ===============================
|
||||||
|
function applyFilters(photos) {
|
||||||
switch (window.currentFilter) {
|
if (!window.currentFilter) return photos;
|
||||||
case "folder":
|
|
||||||
return photos.filter(p => p.folder || (p.path && p.path.includes('/photos/')));
|
switch (window.currentFilter) {
|
||||||
case "location":
|
case "folder":
|
||||||
return photos.filter(p => p?.gps && p.gps.lat);
|
return photos.filter(p => p.cartella);
|
||||||
case "type":
|
case "location":
|
||||||
return photos.filter(p => p?.mime_type && p.mime_type.startsWith("image/"));
|
return photos.filter(p => p.lat && p.lon);
|
||||||
default:
|
case "type":
|
||||||
return photos;
|
return photos.filter(p => p?.mime_type?.startsWith("image/"));
|
||||||
}
|
default:
|
||||||
}
|
return photos;
|
||||||
|
}
|
||||||
// RAGGRUPPAMENTO STILE GOOGLE PHOTOS
|
}
|
||||||
function groupByDate(photos, mode = "auto") {
|
|
||||||
const sections = [];
|
// ===============================
|
||||||
const now = new Date();
|
// 5. RAGGRUPPAMENTO
|
||||||
|
// ===============================
|
||||||
function getLabel(photo) {
|
function groupByDate(photos, mode = "auto") {
|
||||||
const date = photo?.taken_at ? new Date(photo.taken_at) : null;
|
const sections = [];
|
||||||
if (!date || isNaN(+date)) return "Senza data";
|
const now = new Date();
|
||||||
|
|
||||||
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
function getLabel(photo) {
|
||||||
if (mode === "day") return formatDay(date);
|
const date = photo?.taken_at ? new Date(photo.taken_at) : null;
|
||||||
if (mode === "month") return formatMonth(date);
|
if (!date || isNaN(+date)) return "Senza data";
|
||||||
if (mode === "year") return date.getFullYear().toString();
|
|
||||||
|
const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));
|
||||||
if (diffDays === 0) return "Oggi";
|
|
||||||
if (diffDays === 1) return "Ieri";
|
if (mode === "day") return formatDay(date);
|
||||||
if (diffDays <= 7) return "Questa settimana";
|
if (mode === "month") return formatMonth(date);
|
||||||
if (diffDays <= 14) return "La settimana scorsa";
|
if (mode === "year") return date.getFullYear().toString();
|
||||||
if (diffDays <= 30) return "Questo mese";
|
|
||||||
if (diffDays <= 60) return "Mese scorso";
|
if (diffDays === 0) return "Oggi";
|
||||||
|
if (diffDays === 1) return "Ieri";
|
||||||
if (date.getFullYear() === now.getFullYear()) return formatMonth(date);
|
if (diffDays <= 7) return "Questa settimana";
|
||||||
return date.getFullYear().toString();
|
if (diffDays <= 14) return "La settimana scorsa";
|
||||||
}
|
if (diffDays <= 30) return "Questo mese";
|
||||||
|
if (diffDays <= 60) return "Mese scorso";
|
||||||
photos.forEach(photo => {
|
|
||||||
const label = getLabel(photo);
|
if (date.getFullYear() === now.getFullYear()) return formatMonth(date);
|
||||||
let section = sections.find(s => s.label === label);
|
return date.getFullYear().toString();
|
||||||
if (!section) {
|
}
|
||||||
section = { label, photos: [] };
|
|
||||||
sections.push(section);
|
photos.forEach(photo => {
|
||||||
}
|
const label = getLabel(photo);
|
||||||
section.photos.push(photo);
|
let section = sections.find(s => s.label === label);
|
||||||
});
|
if (!section) {
|
||||||
|
section = { label, photos: [] };
|
||||||
return sections;
|
sections.push(section);
|
||||||
}
|
}
|
||||||
|
section.photos.push(photo);
|
||||||
// FORMATTATORI
|
});
|
||||||
function formatDay(date) {
|
|
||||||
return date.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" });
|
return sections;
|
||||||
}
|
}
|
||||||
function formatMonth(date) {
|
|
||||||
return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" });
|
// ===============================
|
||||||
}
|
// 6. FORMATTATORI
|
||||||
|
// ===============================
|
||||||
// RENDER
|
function formatDay(date) {
|
||||||
function renderGallery(sections) {
|
return date.toLocaleDateString("it-IT", { weekday: "long", day: "numeric", month: "long" });
|
||||||
const gallery = document.getElementById("gallery");
|
}
|
||||||
if (!gallery) return;
|
function formatMonth(date) {
|
||||||
gallery.innerHTML = "";
|
return date.toLocaleDateString("it-IT", { month: "long", year: "numeric" });
|
||||||
|
}
|
||||||
sections.forEach(section => {
|
|
||||||
const h = document.createElement("h2");
|
// ===============================
|
||||||
h.className = "gallery-section-title";
|
// 7. RENDER GALLERY
|
||||||
h.textContent = section.label;
|
// ===============================
|
||||||
gallery.appendChild(h);
|
function renderGallery(sections) {
|
||||||
|
const gallery = document.getElementById("gallery");
|
||||||
const container = document.createElement("div");
|
if (!gallery) return;
|
||||||
container.className = "gallery-section";
|
gallery.innerHTML = "";
|
||||||
|
|
||||||
section.photos.forEach((photo, idx) => {
|
sections.forEach(section => {
|
||||||
const thumbDiv = document.createElement("div");
|
const h = document.createElement("h2");
|
||||||
thumbDiv.className = "thumb";
|
h.className = "gallery-section-title";
|
||||||
|
h.textContent = section.label;
|
||||||
// const th1 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub1) : photo?.thub1;
|
gallery.appendChild(h);
|
||||||
// const th2 = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.thub2 || photo?.thub1) : (photo?.thub2 || photo?.thub1);
|
|
||||||
// const original = (typeof toAbsoluteUrl === 'function') ? toAbsoluteUrl(photo?.path) : photo?.path;
|
const container = document.createElement("div");
|
||||||
|
container.className = "gallery-section";
|
||||||
let th1, th2, original;
|
|
||||||
|
section.photos.forEach((photo, idx) => {
|
||||||
if (window.PATH_FULL) {
|
const thumbDiv = document.createElement("div");
|
||||||
// Uso direttamente i path completi generati dal backend
|
thumbDiv.className = "thumb";
|
||||||
th1 = photo?.thub1;
|
thumbDiv.id = "photo_" + photo.id;
|
||||||
th2 = photo?.thub2 || photo?.thub1;
|
|
||||||
original = photo?.path;
|
let th1 = photo?.thub1;
|
||||||
} else {
|
let th2 = photo?.thub2 || photo?.thub1;
|
||||||
// Comportamento attuale: costruisco URL con toAbsoluteUrl()
|
let original = photo?.path;
|
||||||
th1 = (typeof toAbsoluteUrl === 'function')
|
|
||||||
? toAbsoluteUrl(photo?.thub1, photo?.user, "thumbs", photo?.cartella)
|
const img = document.createElement("img");
|
||||||
: photo?.thub1;
|
img.src = th1 || th2 || original || "";
|
||||||
|
img.alt = photo?.name || "";
|
||||||
th2 = (typeof toAbsoluteUrl === 'function')
|
img.loading = "lazy";
|
||||||
? toAbsoluteUrl(photo?.thub2 || photo?.thub1, photo?.user, "thumbs", photo?.cartella)
|
thumbDiv.appendChild(img);
|
||||||
: (photo?.thub2 || photo?.thub1);
|
|
||||||
|
if (photo?.mime_type?.startsWith("video/")) {
|
||||||
original = (typeof toAbsoluteUrl === 'function')
|
const play = document.createElement("div");
|
||||||
? toAbsoluteUrl(photo?.path, photo?.user, "original", photo?.cartella)
|
play.className = "play-icon";
|
||||||
: photo?.path;
|
play.textContent = "▶";
|
||||||
}
|
thumbDiv.appendChild(play);
|
||||||
|
}
|
||||||
|
|
||||||
//console.log(photo?.user);
|
thumbDiv.addEventListener("click", () => {
|
||||||
console.log(th1);
|
window.closeBottomSheet?.();
|
||||||
|
|
||||||
const img = document.createElement("img");
|
if (typeof window.openModalFromList === "function") {
|
||||||
img.src = th1 || th2 || original || "";
|
window.openModalFromList(section.photos, idx);
|
||||||
img.alt = photo?.name || "";
|
} else {
|
||||||
img.loading = "lazy";
|
window.openModal?.(original, th2, photo);
|
||||||
thumbDiv.appendChild(img);
|
}
|
||||||
|
});
|
||||||
if (photo?.mime_type && photo.mime_type.startsWith("video/")) {
|
|
||||||
const play = document.createElement("div");
|
container.appendChild(thumbDiv);
|
||||||
play.className = "play-icon";
|
});
|
||||||
play.textContent = "▶";
|
|
||||||
thumbDiv.appendChild(play);
|
gallery.appendChild(container);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
thumbDiv.addEventListener("click", () => {
|
|
||||||
// Chiudi sempre la strip prima di aprire una nuova foto
|
// ===============================
|
||||||
window.closeBottomSheet?.();
|
// 8. REFRESH GALLERY
|
||||||
|
// ===============================
|
||||||
if (typeof window.openModalFromList === "function") {
|
function refreshGallery() {
|
||||||
window.openModalFromList(section.photos, idx);
|
const photos = getLocalPhotos(); // 🔥 USO DELLO STATO UNICO
|
||||||
} else {
|
console.log("[refreshGallery] numero foto:", photos.length);
|
||||||
window.openModal?.(original, th2, photo);
|
|
||||||
}
|
const filtered = applyFilters(photos);
|
||||||
});
|
const sorted = sortByDate(filtered, "desc");
|
||||||
|
const sections = groupByDate(sorted, "auto");
|
||||||
container.appendChild(thumbDiv);
|
renderGallery(sections);
|
||||||
});
|
}
|
||||||
|
|
||||||
gallery.appendChild(container);
|
// ===============================
|
||||||
});
|
// 9. INIZIALIZZAZIONE GALLERY
|
||||||
}
|
// ===============================
|
||||||
|
async function initGallery() {
|
||||||
// Esporti su window
|
console.log("=== INIT GALLERY ===");
|
||||||
window.sortByDate = sortByDate;
|
|
||||||
window.applyFilters = applyFilters;
|
console.log("[initGallery] Chiamo /config...");
|
||||||
window.groupByDate = groupByDate;
|
const cfg = await fetch("/config").then(r => r.json());
|
||||||
window.renderGallery = renderGallery;
|
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;
|
||||||
|
|
@ -1,293 +1,260 @@
|
||||||
// ===============================
|
// ===============================
|
||||||
// AVVIO
|
// AVVIO
|
||||||
// ===============================
|
// ===============================
|
||||||
console.log("main.js avviato");
|
console.log("main.js avviato");
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
// UTILS AUTH + SYNC HEADER UI
|
// UTILS AUTH + SYNC HEADER UI
|
||||||
// ===============================
|
// ===============================
|
||||||
function isAuthenticated() {
|
function isAuthenticated() {
|
||||||
// Fonte verità: presenza del token in localStorage
|
return !!localStorage.getItem("token");
|
||||||
return !!localStorage.getItem("token");
|
}
|
||||||
}
|
|
||||||
|
function syncHeaderAuthUI() {
|
||||||
/**
|
const authed = isAuthenticated();
|
||||||
* Sincronizza l’UI dell’header in base allo stato auth:
|
document.body.classList.toggle('authenticated', authed);
|
||||||
* - Aggiunge una classe sul body (CSS-friendly)
|
|
||||||
* - Mostra/Nasconde il bottone logout (hotfix inline, se vuoi puoi affidarti solo al CSS)
|
const logoutBtn = document.getElementById('logoutBtn');
|
||||||
*/
|
if (logoutBtn) {
|
||||||
function syncHeaderAuthUI() {
|
logoutBtn.style.display = authed ? 'inline-flex' : 'none';
|
||||||
const authed = isAuthenticated();
|
}
|
||||||
document.body.classList.toggle('authenticated', authed);
|
}
|
||||||
|
|
||||||
const logoutBtn = document.getElementById('logoutBtn');
|
// ===============================
|
||||||
if (logoutBtn) {
|
// PATCH HEADER HEIGHT
|
||||||
// Hotfix immediato: forza la visibilità anche via inline style
|
// ===============================
|
||||||
// (puoi rimuovere queste due righe se preferisci usare solo la regola CSS body.authenticated #logoutBtn)
|
(function () {
|
||||||
logoutBtn.style.display = authed ? 'inline-flex' : 'none';
|
const root = document.documentElement;
|
||||||
}
|
const header = document.querySelector('header');
|
||||||
}
|
|
||||||
|
function setHeaderHeight() {
|
||||||
// ===============================
|
const h = header ? Math.round(header.getBoundingClientRect().height) : 60;
|
||||||
// PATCH: misura l'altezza reale dell'header e aggiorna --header-h
|
root.style.setProperty('--header-h', h + 'px');
|
||||||
// (serve per far partire la mappa subito sotto l’header, anche su mobile)
|
}
|
||||||
// ===============================
|
|
||||||
(function () {
|
setHeaderHeight();
|
||||||
const root = document.documentElement;
|
|
||||||
const header = document.querySelector('header');
|
if (window.ResizeObserver && header) {
|
||||||
|
const ro = new ResizeObserver(setHeaderHeight);
|
||||||
function setHeaderHeight() {
|
ro.observe(header);
|
||||||
const h = header ? Math.round(header.getBoundingClientRect().height) : 60;
|
} else {
|
||||||
root.style.setProperty('--header-h', h + 'px');
|
window.addEventListener('resize', setHeaderHeight);
|
||||||
}
|
window.addEventListener('orientationchange', setHeaderHeight);
|
||||||
|
}
|
||||||
setHeaderHeight();
|
|
||||||
|
window.addEventListener('load', setHeaderHeight);
|
||||||
if (window.ResizeObserver && header) {
|
})();
|
||||||
const ro = new ResizeObserver(setHeaderHeight);
|
|
||||||
ro.observe(header);
|
// ===============================
|
||||||
} else {
|
// PATCH MAP invalidateSize
|
||||||
window.addEventListener('resize', setHeaderHeight);
|
// ===============================
|
||||||
window.addEventListener('orientationchange', setHeaderHeight);
|
(function () {
|
||||||
}
|
const mapEl = document.getElementById('globalMap');
|
||||||
|
if (!mapEl) return;
|
||||||
window.addEventListener('load', setHeaderHeight);
|
|
||||||
})();
|
function invalidateWhenOpen() {
|
||||||
|
if (!mapEl.classList.contains('open')) return;
|
||||||
// ===============================
|
setTimeout(() => {
|
||||||
// PATCH: quando si apre la mappa (#globalMap.open) invalida le dimensioni Leaflet
|
try {
|
||||||
// (utile perché prima era display:none; invalidateSize evita “tagli” o tile sfasati)
|
window.leafletMapInstance?.invalidateSize();
|
||||||
// ===============================
|
} catch (e) {
|
||||||
(function () {
|
console.warn('invalidateSize non eseguito:', e);
|
||||||
const mapEl = document.getElementById('globalMap');
|
}
|
||||||
if (!mapEl) return;
|
}, 0);
|
||||||
|
}
|
||||||
function invalidateWhenOpen() {
|
|
||||||
if (!mapEl.classList.contains('open')) return;
|
const mo = new MutationObserver((mutations) => {
|
||||||
// Aspetta un tick così il layout è aggiornato
|
if (mutations.some(m => m.attributeName === 'class')) {
|
||||||
setTimeout(() => {
|
invalidateWhenOpen();
|
||||||
try {
|
}
|
||||||
// In mapGlobal.js imposta: window.leafletMapInstance = window.globalMap;
|
});
|
||||||
window.leafletMapInstance?.invalidateSize();
|
mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] });
|
||||||
} catch (e) {
|
|
||||||
console.warn('invalidateSize non eseguito:', e);
|
document.getElementById('openMapBtn')?.addEventListener('click', () => {
|
||||||
}
|
setTimeout(invalidateWhenOpen, 0);
|
||||||
}, 0);
|
});
|
||||||
}
|
})();
|
||||||
|
|
||||||
// 1) Osserva il cambio classe (quando aggiungi .open)
|
// ===============================
|
||||||
const mo = new MutationObserver((mutations) => {
|
// MENU ⋮
|
||||||
if (mutations.some(m => m.attributeName === 'class')) {
|
// ===============================
|
||||||
invalidateWhenOpen();
|
(() => {
|
||||||
}
|
const optBtn = document.getElementById("optionsBtn");
|
||||||
});
|
const optSheet = document.getElementById("optionsSheet");
|
||||||
mo.observe(mapEl, { attributes: true, attributeFilter: ['class'] });
|
const overlayEl= document.getElementById("sheetOverlay");
|
||||||
|
|
||||||
// 2) Fallback: se usi il bottone #openMapBtn per aprire/chiudere
|
if (!optBtn || !optSheet) return;
|
||||||
document.getElementById('openMapBtn')?.addEventListener('click', () => {
|
|
||||||
setTimeout(invalidateWhenOpen, 0);
|
function openOptionsSheet() {
|
||||||
});
|
try { window.closeBottomSheet?.(); } catch {}
|
||||||
})();
|
optSheet.classList.add("open");
|
||||||
|
overlayEl?.classList.add("open");
|
||||||
// ===============================
|
optBtn.setAttribute("aria-expanded", "true");
|
||||||
// MENU ⋮ (toggle apri/chiudi con lo stesso bottone, senza global conflicts)
|
optSheet.setAttribute("aria-hidden", "false");
|
||||||
// ===============================
|
}
|
||||||
(() => {
|
|
||||||
const optBtn = document.getElementById("optionsBtn");
|
function closeOptionsSheet() {
|
||||||
const optSheet = document.getElementById("optionsSheet");
|
optSheet.classList.remove("open");
|
||||||
const overlayEl= document.getElementById("sheetOverlay");
|
overlayEl?.classList.remove("open");
|
||||||
|
optBtn.setAttribute("aria-expanded", "false");
|
||||||
if (!optBtn || !optSheet) return;
|
optSheet.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
function openOptionsSheet() {
|
|
||||||
try { window.closeBottomSheet?.(); } catch {}
|
function toggleOptionsSheet(e) {
|
||||||
optSheet.classList.add("open");
|
e?.preventDefault();
|
||||||
overlayEl?.classList.add("open");
|
e?.stopPropagation();
|
||||||
// ARIA (facoltativo)
|
if (optSheet.classList.contains("open")) closeOptionsSheet();
|
||||||
optBtn.setAttribute("aria-expanded", "true");
|
else openOptionsSheet();
|
||||||
optSheet.setAttribute("aria-hidden", "false");
|
}
|
||||||
}
|
|
||||||
|
optBtn.addEventListener("click", toggleOptionsSheet, { capture: true });
|
||||||
function closeOptionsSheet() {
|
overlayEl?.addEventListener("click", (e) => {
|
||||||
optSheet.classList.remove("open");
|
e.stopPropagation();
|
||||||
overlayEl?.classList.remove("open");
|
closeOptionsSheet();
|
||||||
// ARIA (facoltativo)
|
});
|
||||||
optBtn.setAttribute("aria-expanded", "false");
|
|
||||||
optSheet.setAttribute("aria-hidden", "true");
|
document.addEventListener("keydown", (e) => {
|
||||||
}
|
if (e.key === "Escape" && optSheet.classList.contains("open")) {
|
||||||
|
closeOptionsSheet();
|
||||||
function toggleOptionsSheet(e) {
|
}
|
||||||
e?.preventDefault();
|
});
|
||||||
e?.stopPropagation();
|
|
||||||
if (optSheet.classList.contains("open")) closeOptionsSheet();
|
optSheet.addEventListener("click", (e) => e.stopPropagation());
|
||||||
else openOptionsSheet();
|
window.closeOptionsSheet = closeOptionsSheet;
|
||||||
}
|
})();
|
||||||
|
|
||||||
// Click sul bottone: toggle (fase di cattura per battere eventuali altri handler)
|
// ===============================
|
||||||
optBtn.addEventListener("click", toggleOptionsSheet, { capture: true });
|
// LOGIN AUTOMATICO SU INDEX
|
||||||
|
// ===============================
|
||||||
// Chiudi clic overlay
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
overlayEl?.addEventListener("click", (e) => {
|
syncHeaderAuthUI();
|
||||||
e.stopPropagation();
|
|
||||||
closeOptionsSheet();
|
try {
|
||||||
});
|
const cfgRes = await fetch('/config');
|
||||||
|
const cfg = await cfgRes.json();
|
||||||
// Chiudi con ESC
|
window.BASE_URL = cfg.baseUrl;
|
||||||
document.addEventListener("keydown", (e) => {
|
|
||||||
if (e.key === "Escape" && optSheet.classList.contains("open")) {
|
const savedToken = localStorage.getItem("token");
|
||||||
closeOptionsSheet();
|
|
||||||
}
|
if (!savedToken) {
|
||||||
});
|
document.getElementById("loginModal").style.display = "flex";
|
||||||
|
return;
|
||||||
// Evita chiusure involontarie per click interni
|
}
|
||||||
optSheet.addEventListener("click", (e) => e.stopPropagation());
|
|
||||||
|
const ping = await fetch(`${window.BASE_URL}/photos`, {
|
||||||
// Espone una close per usarla altrove (es. dopo la scelta)
|
headers: { "Authorization": "Bearer " + savedToken }
|
||||||
window.closeOptionsSheet = closeOptionsSheet;
|
});
|
||||||
})();
|
|
||||||
|
if (!ping.ok) {
|
||||||
// ===============================
|
localStorage.removeItem("token");
|
||||||
// LOGIN AUTOMATICO SU INDEX
|
syncHeaderAuthUI();
|
||||||
// ===============================
|
document.getElementById("loginModal").style.display = "flex";
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
return;
|
||||||
// Allinea subito l’UI in base al token eventualmente già presente
|
}
|
||||||
syncHeaderAuthUI();
|
|
||||||
|
window.token = savedToken;
|
||||||
try {
|
syncHeaderAuthUI();
|
||||||
// 1) Carica config
|
|
||||||
const cfgRes = await fetch('/config');
|
// 🔥 NON caricare più foto da main.js
|
||||||
const cfg = await cfgRes.json();
|
// La gallery viene caricata da initGallery() in gallery.js
|
||||||
window.BASE_URL = cfg.baseUrl;
|
|
||||||
|
} catch (err) {
|
||||||
// 2) Recupera token salvato
|
console.error("Errore autenticazione:", err);
|
||||||
const savedToken = localStorage.getItem("token");
|
document.getElementById("loginModal").style.display = "flex";
|
||||||
|
}
|
||||||
// Se non c'è token → mostra login
|
});
|
||||||
if (!savedToken) {
|
|
||||||
document.getElementById("loginModal").style.display = "flex";
|
// ===============================
|
||||||
return;
|
// SETTINGS (⚙️)
|
||||||
}
|
// ===============================
|
||||||
|
document.getElementById('settingsBtn')?.addEventListener('click', () => {
|
||||||
// 3) Verifica token
|
window.location.href = "admin.html";
|
||||||
const ping = await fetch(`${window.BASE_URL}/photos`, {
|
});
|
||||||
headers: { "Authorization": "Bearer " + savedToken }
|
|
||||||
});
|
// ===============================
|
||||||
|
// LOGIN SUBMIT
|
||||||
if (!ping.ok) {
|
// ===============================
|
||||||
// Token invalido → cancella e mostra login
|
/*
|
||||||
localStorage.removeItem("token");
|
document.getElementById("loginSubmit").addEventListener("click", async () => {
|
||||||
syncHeaderAuthUI(); // riallinea header subito
|
const email = document.getElementById("loginEmail").value;
|
||||||
document.getElementById("loginModal").style.display = "flex";
|
const password = document.getElementById("loginPassword").value;
|
||||||
return;
|
const errorEl = document.getElementById("loginError");
|
||||||
}
|
|
||||||
|
errorEl.textContent = "";
|
||||||
// 4) Token valido → salva e carica gallery
|
|
||||||
window.token = savedToken;
|
try {
|
||||||
syncHeaderAuthUI(); // <— mostra il logout senza refresh
|
const res = await fetch(`${window.BASE_URL}/auth/login`, {
|
||||||
loadPhotos();
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
} catch (err) {
|
body: JSON.stringify({ email, password })
|
||||||
console.error("Errore autenticazione:", err);
|
});
|
||||||
document.getElementById("loginModal").style.display = "flex";
|
|
||||||
}
|
if (!res.ok) {
|
||||||
});
|
errorEl.textContent = "Utente o password errati";
|
||||||
|
return;
|
||||||
// ===============================
|
}
|
||||||
// VARIABILI GLOBALI
|
|
||||||
// ===============================
|
const data = await res.json();
|
||||||
let currentSort = "desc";
|
const token = data.token;
|
||||||
let currentGroup = "auto";
|
|
||||||
let currentFilter = null;
|
localStorage.setItem("token", token);
|
||||||
|
window.token = token;
|
||||||
window.currentSort = currentSort;
|
|
||||||
window.currentGroup = currentGroup;
|
document.getElementById("loginModal").style.display = "none";
|
||||||
window.currentFilter = currentFilter;
|
|
||||||
|
syncHeaderAuthUI();
|
||||||
// ===============================
|
|
||||||
// BOTTONI OPZIONI
|
// 🔥 NON caricare più foto da main.js
|
||||||
// ===============================
|
// initGallery() farà tutto
|
||||||
document.querySelectorAll("#optionsSheet .sheet-btn").forEach(btn => {
|
|
||||||
btn.addEventListener("click", () => {
|
} catch (err) {
|
||||||
if (btn.dataset.sort) window.currentSort = currentSort = btn.dataset.sort;
|
console.error("Errore login:", err);
|
||||||
if (btn.dataset.group) window.currentGroup = currentGroup = btn.dataset.group;
|
errorEl.textContent = "Errore di connessione al server";
|
||||||
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();
|
// LOGIN SUBMIT
|
||||||
});
|
// ===============================
|
||||||
});
|
document.getElementById("loginSubmit").addEventListener("click", async () => {
|
||||||
|
const email = document.getElementById("loginEmail").value;
|
||||||
// ===============================
|
const password = document.getElementById("loginPassword").value;
|
||||||
// REFRESH GALLERY
|
const errorEl = document.getElementById("loginError");
|
||||||
// ===============================
|
|
||||||
function refreshGallery() {
|
errorEl.textContent = "";
|
||||||
console.log("Aggiornamento galleria...");
|
|
||||||
|
try {
|
||||||
const data = Array.isArray(window.photosData) ? window.photosData : [];
|
const res = await fetch(`${window.BASE_URL}/auth/login`, {
|
||||||
let photos = [...data];
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
if (typeof applyFilters === 'function') photos = applyFilters(photos);
|
body: JSON.stringify({ email, password })
|
||||||
if (typeof sortByDate === 'function') photos = sortByDate(photos, currentSort);
|
});
|
||||||
|
|
||||||
let sections = [{ label: 'Tutte', photos }];
|
if (!res.ok) {
|
||||||
if (typeof groupByDate === 'function') sections = groupByDate(photos, currentGroup);
|
errorEl.textContent = "Utente o password errati";
|
||||||
|
return;
|
||||||
if (typeof renderGallery === 'function') {
|
}
|
||||||
renderGallery(sections);
|
|
||||||
}
|
const data = await res.json();
|
||||||
}
|
const token = data.token;
|
||||||
|
|
||||||
window.refreshGallery = refreshGallery;
|
// Salva token
|
||||||
|
localStorage.setItem("token", token);
|
||||||
// ===============================
|
window.token = token;
|
||||||
// SETTINGS (⚙️) — apre admin.html
|
|
||||||
// ===============================
|
// Chiudi login
|
||||||
const settingsBtn = document.getElementById('settingsBtn');
|
const loginModalEl = document.getElementById("loginModal");
|
||||||
settingsBtn?.addEventListener('click', () => {
|
if (loginModalEl) loginModalEl.style.display = "none";
|
||||||
window.location.href = "admin.html";
|
|
||||||
});
|
// Riallinea UI header subito
|
||||||
|
syncHeaderAuthUI();
|
||||||
// ===============================
|
|
||||||
// LOGIN SUBMIT
|
// 🔥 RICARICA LA PAGINA PER FAR PARTIRE initGallery()
|
||||||
// ===============================
|
window.location.reload();
|
||||||
document.getElementById("loginSubmit").addEventListener("click", async () => {
|
|
||||||
const email = document.getElementById("loginEmail").value;
|
} catch (err) {
|
||||||
const password = document.getElementById("loginPassword").value;
|
console.error("Errore login:", err);
|
||||||
const errorEl = document.getElementById("loginError");
|
errorEl.textContent = "Errore di connessione al server";
|
||||||
|
}
|
||||||
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;
|
|
||||||
|
|
||||||
// Salva token
|
|
||||||
localStorage.setItem("token", token);
|
|
||||||
window.token = token;
|
|
||||||
|
|
||||||
// Chiudi login
|
|
||||||
const loginModalEl = document.getElementById("loginModal");
|
|
||||||
if (loginModalEl) loginModalEl.style.display = "none";
|
|
||||||
|
|
||||||
// Riallinea UI header subito (mostra logout) e carica gallery
|
|
||||||
syncHeaderAuthUI();
|
|
||||||
loadPhotos();
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Errore login:", err);
|
|
||||||
errorEl.textContent = "Errore di connessione al server";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
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
|
||||||