modulare funzionante da verificare admin.html
11
.env
|
|
@ -22,8 +22,17 @@ LOG_FILE=scan.log
|
|||
LOG_DIR=public
|
||||
LOG_VERBOSE=true
|
||||
|
||||
# POLLING_TIME in secondi
|
||||
# Funzionamento:
|
||||
# - POLLING_TIME = 0 → polling disattivato (solo WebSocket)
|
||||
# - POLLING_TIME > 0 → esegue incrementalSync() ogni N secondi
|
||||
|
||||
GALLERY_REFRESH_SECONDS=30
|
||||
# Perché esiste?
|
||||
# - Serve come "rete di sicurezza" se il WebSocket cade,
|
||||
# se la tab viene sospesa, o se qualche evento viene perso.
|
||||
# - Google Photos, Dropbox e iCloud usano lo stesso approccio.
|
||||
GALLERY_REFRESH_SECONDS=300
|
||||
|
||||
WS_PORT=4002
|
||||
WS_HOST=0.0.0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
lock
|
||||
BIN
Viaggi-2019-Urbino/IMG_20191214_151914.jpg
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_152503.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_152514.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_170322.jpg
Normal file
|
After Width: | Height: | Size: 294 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_170411.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_170509.jpg
Normal file
|
After Width: | Height: | Size: 641 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_170601.jpg
Normal file
|
After Width: | Height: | Size: 1 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_171916.jpg
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_174249.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_174310.jpg
Normal file
|
After Width: | Height: | Size: 719 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_174319.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_174323.jpg
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_194046.jpg
Normal file
|
After Width: | Height: | Size: 306 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_194113.jpg
Normal file
|
After Width: | Height: | Size: 355 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_194146.jpg
Normal file
|
After Width: | Height: | Size: 390 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_194227.jpg
Normal file
|
After Width: | Height: | Size: 402 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_194254.jpg
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_195438.jpg
Normal file
|
After Width: | Height: | Size: 326 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_195454.jpg
Normal file
|
After Width: | Height: | Size: 368 KiB |
BIN
Viaggi-2019-Urbino/IMG_20191214_212249.jpg
Normal file
|
After Width: | Height: | Size: 485 KiB |
BIN
Viaggi-2019-Urbino/PANO_20191214_174001.jpg
Normal file
|
After Width: | Height: | Size: 896 KiB |
64
api_v1/scanner/debugVideoDates.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
// api_v1/scanner/debugVideoDates.js
|
||||
const path = require('path');
|
||||
const { probeVideo } = require('./video');
|
||||
|
||||
async function debugVideo(absPath) {
|
||||
console.log("🎥 File:", absPath);
|
||||
|
||||
const info = await probeVideo(absPath);
|
||||
|
||||
console.log("\n=== RAW probeVideo(info) ===\n");
|
||||
console.dir(info, { depth: 6 });
|
||||
|
||||
// Estrazione campi data come nello scanner
|
||||
const formatTags = info.format?.tags || {};
|
||||
const stream0Tags = info.streams?.[0]?.tags || {};
|
||||
const stream1Tags = info.streams?.[1]?.tags || {};
|
||||
|
||||
const creationFormat = formatTags.creation_time || null;
|
||||
const creationStream0 = stream0Tags.creation_time || null;
|
||||
const creationStream1 = stream1Tags.creation_time || null;
|
||||
|
||||
console.log("\n=== DATE CANDIDATE ===");
|
||||
console.log("format.tags.creation_time :", creationFormat);
|
||||
console.log("streams[0].tags.creation_time:", creationStream0);
|
||||
console.log("streams[1].tags.creation_time:", creationStream1);
|
||||
|
||||
// Simulazione logica attuale dello scanner (ma esplicita)
|
||||
let videoDate =
|
||||
creationFormat ||
|
||||
creationStream0 ||
|
||||
creationStream1 ||
|
||||
null;
|
||||
|
||||
console.log("\nScelta videoDate (prima di qualsiasi conversione):", videoDate);
|
||||
|
||||
if (videoDate) {
|
||||
// Variante 1: SENZA conversione (quella che ti ho suggerito)
|
||||
const takenAtRaw = videoDate;
|
||||
|
||||
// Variante 2: CON conversione (quella che ti sballa il giorno)
|
||||
const takenAtIso = new Date(videoDate).toISOString();
|
||||
|
||||
console.log("\n=== CONFRONTO ===");
|
||||
console.log("takenAtRaw (usato così com'è) :", takenAtRaw);
|
||||
console.log("takenAtIso (new Date().toISOString):", takenAtIso);
|
||||
} else {
|
||||
console.log("\n⚠ Nessuna data trovata nei tag video.");
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const arg = process.argv[2];
|
||||
if (!arg) {
|
||||
console.error("Uso: node debugVideoDates.js /percorso/al/video.mp4");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const absPath = path.resolve(arg);
|
||||
await debugVideo(absPath);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error("❌ Errore debugVideoDates:", err);
|
||||
});
|
||||
55
api_v1/scanner/deleteFolder.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// api_v1/scanner/deleteFolder.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const { WEB_ROOT } = require('../config');
|
||||
|
||||
module.exports = function createDeleteFolderFunctions(db) {
|
||||
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
const { deleteFromDB } = createCleanupFunctions(db);
|
||||
const recordChange = require('./recordChange');
|
||||
|
||||
/**
|
||||
* Cancella una cartella per un utente:
|
||||
* - elimina tutti i record DB
|
||||
* - registra removed
|
||||
* - elimina la cartella thumbs residua
|
||||
*/
|
||||
async function deleteFolderForUser(userName, folderName) {
|
||||
|
||||
// 1) Recupera tutti gli ID nel DB
|
||||
const rows = await db('photos')
|
||||
.where({ user: userName, cartella: folderName })
|
||||
.select('id');
|
||||
|
||||
// 2) Cancella record DB + registra removed
|
||||
for (const r of rows) {
|
||||
const id = r.id;
|
||||
|
||||
await deleteFromDB(id, userName);
|
||||
await recordChange(db, id, userName, "removed");
|
||||
}
|
||||
|
||||
// 3) Cancella la cartella thumbs residua
|
||||
const thumbsDir = path.join(
|
||||
WEB_ROOT,
|
||||
userName,
|
||||
"thumbs",
|
||||
folderName
|
||||
);
|
||||
|
||||
try {
|
||||
await fsp.rm(thumbsDir, { recursive: true, force: true });
|
||||
console.log(`🧹 Rimossa cartella thumbs residua: ${thumbsDir}`);
|
||||
} catch (err) {
|
||||
console.log(`⚠️ Errore rimozione thumbs dir: ${thumbsDir}`, err);
|
||||
}
|
||||
|
||||
return {
|
||||
removedRecords: rows.length,
|
||||
removedThumbsDir: thumbsDir
|
||||
};
|
||||
}
|
||||
|
||||
return { deleteFolderForUser };
|
||||
};
|
||||
|
|
@ -1,22 +1,81 @@
|
|||
// elevation.js
|
||||
async function getElevation(lat, lon) {
|
||||
console.log("OpenElevation request:", lat, lon);
|
||||
|
||||
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
console.log("OpenElevation status:", res.status);
|
||||
|
||||
const json = await res.json();
|
||||
console.log("OpenElevation response:", json);
|
||||
|
||||
return json?.results?.[0]?.elevation ?? null;
|
||||
|
||||
} catch (err) {
|
||||
console.warn("Errore OpenElevation:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getElevation };
|
||||
// api_v1/scanner/elevation.js
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Provider 1: OpenElevation
|
||||
// ---------------------------------------------------------
|
||||
async function tryOpenElevation(lat, lon) {
|
||||
try {
|
||||
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`;
|
||||
const res = await fetch(url, { timeout: 5000 });
|
||||
|
||||
if (!res.ok) {
|
||||
console.log("OpenElevation status:", res.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
|
||||
// Se non è JSON → errore HTML → fallback
|
||||
if (!text.startsWith('{')) {
|
||||
console.log("OpenElevation non-JSON:", text.slice(0, 60));
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(text);
|
||||
return data?.results?.[0]?.elevation ?? null;
|
||||
|
||||
} catch (err) {
|
||||
console.log("Errore OpenElevation:", err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Provider 2: OpenTopoData (fallback)
|
||||
// ---------------------------------------------------------
|
||||
async function tryOpenTopoData(lat, lon) {
|
||||
try {
|
||||
const url = `https://api.opentopodata.org/v1/eudem25m?locations=${lat},${lon}`;
|
||||
const res = await fetch(url, { timeout: 5000 });
|
||||
|
||||
if (!res.ok) {
|
||||
console.log("OpenTopoData status:", res.status);
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = await res.text();
|
||||
|
||||
if (!text.startsWith('{')) {
|
||||
console.log("OpenTopoData non-JSON:", text.slice(0, 60));
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = JSON.parse(text);
|
||||
return data?.results?.[0]?.elevation ?? null;
|
||||
|
||||
} catch (err) {
|
||||
console.log("Errore OpenTopoData:", err.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Funzione principale con fallback
|
||||
// ---------------------------------------------------------
|
||||
async function getElevation(lat, lon) {
|
||||
// 1️⃣ Prova OpenElevation
|
||||
const elev1 = await tryOpenElevation(lat, lon);
|
||||
if (elev1 !== null) return elev1;
|
||||
|
||||
console.log("⚠️ OpenElevation fallito → uso OpenTopoData");
|
||||
|
||||
// 2️⃣ Prova OpenTopoData
|
||||
const elev2 = await tryOpenTopoData(lat, lon);
|
||||
if (elev2 !== null) return elev2;
|
||||
|
||||
console.log("❌ Nessun provider di elevazione disponibile");
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = getElevation;
|
||||
|
|
|
|||
22
api_v1/scanner/elevation.js.old
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// elevation.js
|
||||
async function getElevation(lat, lon) {
|
||||
console.log("OpenElevation request:", lat, lon);
|
||||
|
||||
const url = `https://api.open-elevation.com/api/v1/lookup?locations=${lat},${lon}`;
|
||||
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
console.log("OpenElevation status:", res.status);
|
||||
|
||||
const json = await res.json();
|
||||
console.log("OpenElevation response:", json);
|
||||
|
||||
return json?.results?.[0]?.elevation ?? null;
|
||||
|
||||
} catch (err) {
|
||||
console.warn("Errore OpenElevation:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getElevation };
|
||||
|
|
@ -8,7 +8,8 @@ const { createVideoThumbnail, createThumbnails } = require('./thumbs');
|
|||
const { probeVideo } = require('./video');
|
||||
const loc = require('../geo.js');
|
||||
const { WEB_ROOT, PATH_FULL } = require('../config');
|
||||
const { getElevation } = require('./elevation');
|
||||
//const { getElevation } = require('./elevation');
|
||||
const getElevation = require('./elevation');
|
||||
|
||||
|
||||
async function processFile(userName, cartella, fileRelPath, absPath, ext, st) {
|
||||
|
|
@ -50,12 +51,35 @@ let takenAtIso = parseExifDateUtc(timeRaw);
|
|||
|
||||
// Fallback per i video
|
||||
if (isVideo) {
|
||||
const fallback = new Date(st.mtimeMs).toISOString();
|
||||
takenAtIso = fallback;
|
||||
timeRaw = fallback;
|
||||
const info = await probeVideo(absPath);
|
||||
|
||||
// Cerca la data nei punti standard
|
||||
const creationFormat = info.format?.tags?.creation_time || null;
|
||||
const creationStream0 = info.streams?.[0]?.tags?.creation_time || null;
|
||||
const creationStream1 = info.streams?.[1]?.tags?.creation_time || null;
|
||||
|
||||
// Scegli la prima disponibile
|
||||
const videoDate =
|
||||
creationFormat ||
|
||||
creationStream0 ||
|
||||
creationStream1 ||
|
||||
null;
|
||||
|
||||
if (videoDate) {
|
||||
// NON convertire, NON usare new Date()
|
||||
takenAtIso = videoDate;
|
||||
timeRaw = videoDate;
|
||||
} else {
|
||||
// Fallback finale: mtime del file
|
||||
const fallback = new Date(st.mtimeMs).toISOString();
|
||||
takenAtIso = fallback;
|
||||
timeRaw = fallback;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// --- GPS ---
|
||||
let gps = null;
|
||||
|
||||
|
|
|
|||
183
api_v1/scanner/processFile.js.old
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const ExifReader = require('exifreader');
|
||||
const sharp = require('sharp');
|
||||
const { sha256, inferMimeFromExt, parseExifDateUtc } = require('./utils');
|
||||
const { extractGpsFromExif, extractGpsWithExiftool } = require('./gps');
|
||||
const { createVideoThumbnail, createThumbnails } = require('./thumbs');
|
||||
const { probeVideo } = require('./video');
|
||||
const loc = require('../geo.js');
|
||||
const { WEB_ROOT, PATH_FULL } = require('../config');
|
||||
//const { getElevation } = require('./elevation');
|
||||
const getElevation = require('./elevation');
|
||||
|
||||
|
||||
async function processFile(userName, cartella, fileRelPath, absPath, ext, st) {
|
||||
const isVideo = ['.mp4', '.mov', '.m4v'].includes(ext);
|
||||
|
||||
const thumbBase = path.join(
|
||||
WEB_ROOT,
|
||||
'photos',
|
||||
userName,
|
||||
'thumbs',
|
||||
cartella,
|
||||
path.dirname(fileRelPath)
|
||||
);
|
||||
|
||||
await fsp.mkdir(thumbBase, { recursive: true });
|
||||
|
||||
// --- Nome file + estensione (dal path ASSOLUTO, sempre corretto) ---
|
||||
const parsed = path.parse(absPath);
|
||||
const baseName = parsed.name; // IMG_0249
|
||||
const extName = parsed.ext; // .JPG
|
||||
|
||||
const absThumbMin = path.join(thumbBase, `${baseName}_min.jpg`);
|
||||
const absThumbAvg = path.join(thumbBase, `${baseName}_avg.jpg`);
|
||||
|
||||
if (isVideo) {
|
||||
await createVideoThumbnail(absPath, absThumbMin, absThumbAvg);
|
||||
} else {
|
||||
await createThumbnails(absPath, absThumbMin, absThumbAvg);
|
||||
}
|
||||
|
||||
// --- EXIF ---
|
||||
let tags = {};
|
||||
try {
|
||||
tags = await ExifReader.load(absPath, { expanded: true });
|
||||
} catch {}
|
||||
|
||||
let timeRaw = tags?.exif?.DateTimeOriginal?.value?.[0] || null;
|
||||
let takenAtIso = parseExifDateUtc(timeRaw);
|
||||
|
||||
// Fallback per i video
|
||||
if (isVideo) {
|
||||
const fallback = new Date(st.mtimeMs).toISOString();
|
||||
takenAtIso = fallback;
|
||||
timeRaw = fallback;
|
||||
}
|
||||
|
||||
|
||||
// --- GPS ---
|
||||
let gps = null;
|
||||
|
||||
if (isVideo) {
|
||||
// i video usano exiftool
|
||||
gps = await extractGpsWithExiftool(absPath);
|
||||
} else {
|
||||
// le foto usano exifreader
|
||||
gps = extractGpsFromExif(tags);
|
||||
}
|
||||
|
||||
// --- ALTITUDINE DA SERVIZIO ESTERNO SE MANCANTE ---
|
||||
if (gps && gps.lat && gps.lng) {
|
||||
if (gps.alt == null) {
|
||||
gps.alt = await getElevation(gps.lat, gps.lng);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- DIMENSIONI & ROTAZIONE ---
|
||||
let width = null, height = null, duration = null, duration_ms = null, rotation = 0;
|
||||
|
||||
if (isVideo) {
|
||||
const info = await probeVideo(absPath);
|
||||
const stream = info.streams?.find(s => s.width && s.height);
|
||||
|
||||
if (stream) {
|
||||
width = stream.width;
|
||||
height = stream.height;
|
||||
|
||||
rotation = 0;
|
||||
|
||||
if (stream?.tags?.rotate) {
|
||||
rotation = Number(stream.tags.rotate);
|
||||
}
|
||||
|
||||
const sdl = stream?.side_data_list;
|
||||
if (sdl && Array.isArray(sdl)) {
|
||||
const rotEntry = sdl.find(d => d.rotation !== undefined);
|
||||
if (rotEntry) rotation = Number(rotEntry.rotation);
|
||||
|
||||
const matrixEntry = sdl.find(d => typeof d.displaymatrix === 'string');
|
||||
if (matrixEntry) {
|
||||
const match = matrixEntry.displaymatrix.match(/rotation of ([\-0-9]+) degrees/i);
|
||||
if (match) rotation = Number(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
rotation = ((rotation % 360) + 360) % 360;
|
||||
}
|
||||
|
||||
duration = info.format?.duration || null;
|
||||
duration_ms = duration ? Math.round(duration * 1000) : null;
|
||||
|
||||
} else {
|
||||
try {
|
||||
const meta = await sharp(absPath).metadata();
|
||||
width = meta.width || null;
|
||||
height = meta.height || null;
|
||||
} catch {}
|
||||
|
||||
try {
|
||||
const raw =
|
||||
tags?.exif?.Orientation?.value ??
|
||||
tags?.image?.Orientation?.value ??
|
||||
tags?.ifd0?.Orientation?.value ??
|
||||
null;
|
||||
|
||||
const val = Array.isArray(raw) ? raw[0] : raw;
|
||||
|
||||
const map = { 1: 0, 3: 180, 6: 90, 8: 270 };
|
||||
rotation = map[val] ?? 0;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const mime_type = inferMimeFromExt(ext);
|
||||
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
||||
|
||||
const location = gps ? await loc(gps.lng, gps.lat) : null;
|
||||
|
||||
//
|
||||
// --- GESTIONE PATH FULL / RELATIVI ---
|
||||
//
|
||||
|
||||
const relPath = fileRelPath;
|
||||
const relThub1 = fileRelPath.replace(/\.[^.]+$/, '_min.jpg');
|
||||
const relThub2 = fileRelPath.replace(/\.[^.]+$/, '_avg.jpg');
|
||||
|
||||
const fullPath = PATH_FULL
|
||||
? path.posix.join('/photos', userName, cartella, fileRelPath)
|
||||
: relPath;
|
||||
|
||||
const fullThub1 = PATH_FULL
|
||||
? path.posix.join('/photos', userName, 'thumbs', cartella, relThub1)
|
||||
: relThub1;
|
||||
|
||||
const fullThub2 = PATH_FULL
|
||||
? path.posix.join('/photos', userName, 'thumbs', cartella, relThub2)
|
||||
: relThub2;
|
||||
|
||||
return {
|
||||
id,
|
||||
user: userName,
|
||||
cartella,
|
||||
name: baseName + extName, // 👈 SEMPRE CORRETTO
|
||||
path: fullPath,
|
||||
thub1: fullThub1,
|
||||
thub2: fullThub2,
|
||||
gps,
|
||||
data: timeRaw,
|
||||
taken_at: takenAtIso,
|
||||
mime_type,
|
||||
width,
|
||||
height,
|
||||
rotation,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs,
|
||||
duration_ms: isVideo ? duration_ms : null,
|
||||
location
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = processFile;
|
||||
29
api_v1/scanner/recordChange.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// api_v1/scanner/recordChange.js
|
||||
const { log } = require('./logger');
|
||||
|
||||
async function recordChange(db, photo_id, user, change_type) {
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
// 🔒 deduplica "removed" per stessa foto/utente
|
||||
if (change_type === 'removed') {
|
||||
const already = await db('photo_changes')
|
||||
.where({ photo_id, user, change_type: 'removed' })
|
||||
.first();
|
||||
|
||||
if (already) {
|
||||
log(`📘 [CHANGES] removed già registrato → ${photo_id}, skip`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await db("photo_changes").insert({
|
||||
photo_id,
|
||||
user,
|
||||
change_type,
|
||||
timestamp
|
||||
});
|
||||
|
||||
log(`📘 [CHANGES] ${change_type} → ${photo_id}`);
|
||||
}
|
||||
|
||||
module.exports = recordChange;
|
||||
210
api_v1/scanner/scanAuto.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const scanCartella = require('./scanCartella');
|
||||
const processFile = require('./processFile');
|
||||
const { sha256 } = require('./utils');
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
const { WEB_ROOT } = require('../config');
|
||||
const { log } = require('./logger');
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// NORMALIZZAZIONE PATH (toglie /app/...)
|
||||
// ---------------------------------------------------------
|
||||
function normalizeRelPath(user, dirPath, fileName) {
|
||||
if (!dirPath) return "";
|
||||
|
||||
let rel = dirPath.replace(/\\/g, "/");
|
||||
|
||||
// Rimuove tutto fino a /photos/
|
||||
rel = rel.replace(/^.*\/photos\//, "");
|
||||
|
||||
// Ora rel = "Fabio/original/2017Irlanda/.../"
|
||||
const prefix = `${user}/original/`;
|
||||
|
||||
if (rel.startsWith(prefix)) {
|
||||
rel = rel.slice(prefix.length);
|
||||
}
|
||||
|
||||
if (rel.startsWith("/")) rel = rel.slice(1);
|
||||
|
||||
// Aggiunge il file
|
||||
if (fileName) {
|
||||
rel = `${rel}/${fileName}`.replace(/\/+/g, "/");
|
||||
}
|
||||
|
||||
return rel; // es: "2017Irlanda19-29ago/IMG_0120.JPG"
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// CALCOLO ID IDENTICO AL VECCHIO SCAN
|
||||
// ---------------------------------------------------------
|
||||
function computeId(user, relPath) {
|
||||
const parts = relPath.split('/').filter(Boolean);
|
||||
const cartella = parts[0];
|
||||
const fileRelPath = parts.slice(1).join('/');
|
||||
const id = sha256(`${user}/${cartella}/${fileRelPath}`);
|
||||
return { id, cartella, fileRelPath };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 1) ADD FILE
|
||||
// ---------------------------------------------------------
|
||||
async function handleAddFile(user, dirPath, fileName, db) {
|
||||
const relPath = normalizeRelPath(user, dirPath, fileName);
|
||||
|
||||
log(`🟦 [ADD] user=${user} file=${relPath}`);
|
||||
|
||||
// 🔥 Ricostruzione identica al vecchio scanPhoto
|
||||
const parts = relPath.split('/');
|
||||
const cartella = parts[0]; // es: "2017Irlanda19-29ago"
|
||||
const fileRelPath = parts.slice(1).join('/'); // es: "IMG_0116.JPG"
|
||||
|
||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||
const absPath = path.join(photosRoot, user, 'original', cartella, fileRelPath);
|
||||
|
||||
const ext = path.extname(fileName).toLowerCase();
|
||||
const st = await fsp.stat(absPath);
|
||||
|
||||
// 🔥 FIX: processFile deve ricevere fileRelPath completo di "original/"
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
path.posix.join('original', cartella, fileRelPath),
|
||||
absPath,
|
||||
ext,
|
||||
st
|
||||
);
|
||||
|
||||
// Inserimento identico al vecchio scanPhoto
|
||||
const row = {
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms ?? null,
|
||||
taken_at: meta.taken_at ?? null,
|
||||
data: meta.data ?? null,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null
|
||||
};
|
||||
|
||||
await db('photos')
|
||||
.insert(row)
|
||||
.onConflict('id')
|
||||
.merge();
|
||||
|
||||
log(`🟢 [ADD] Inserito/aggiornato id=${meta.id}`);
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 2) DELETE FILE
|
||||
// ---------------------------------------------------------
|
||||
async function handleDeleteFile(user, dirPath, fileName, db) {
|
||||
const relPath = normalizeRelPath(user, dirPath, fileName);
|
||||
|
||||
log(`🟥 [DEL] user=${user} file=${relPath}`);
|
||||
|
||||
const { id } = computeId(user, relPath);
|
||||
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
await deleteThumbsById(id);
|
||||
await deleteFromDB(id, user);
|
||||
|
||||
log(`🔴 [DEL] File eliminato id=${id}`);
|
||||
|
||||
return { deleted: id };
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 3) ADD DIRECTORY
|
||||
// ---------------------------------------------------------
|
||||
async function handleAddDir(user, dir, db) {
|
||||
log(`🟦 [ADD_DIR] user=${user} dir=${dir}`);
|
||||
|
||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||
const absDir = path.join(photosRoot, user, 'original', dir);
|
||||
|
||||
log(`📁 [ADD_DIR] absDir=${absDir}`);
|
||||
|
||||
let results = [];
|
||||
|
||||
for await (const f of scanCartella(user, dir, absDir, db)) {
|
||||
log(`📄 [ADD_DIR] File trovato: ${f.relPath}`);
|
||||
|
||||
const meta = await processFile(
|
||||
user,
|
||||
dir,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
f.stat
|
||||
);
|
||||
|
||||
await db('photos').insert(meta).onConflict('id').merge();
|
||||
|
||||
log(`🟢 [ADD_DIR] Inserito/aggiornato id=${meta.id} name=${meta.name}`);
|
||||
|
||||
results.push(meta);
|
||||
}
|
||||
|
||||
log(`🟣 [ADD_DIR] Completato. Files=${results.length}`);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 4) DELETE DIRECTORY
|
||||
// ---------------------------------------------------------
|
||||
async function handleDeleteDir(user, dir, db) {
|
||||
log(`🟥 [DEL_DIR] user=${user} dir=${dir}`);
|
||||
|
||||
const rows = await db('photos')
|
||||
.where({ user, cartella: dir })
|
||||
.select('id');
|
||||
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
log(`📁 [DEL_DIR] Trovati ${rows.length} file da eliminare`);
|
||||
|
||||
for (const r of rows) {
|
||||
log(`🗑️ [DEL_DIR] Eliminazione id=${r.id}`);
|
||||
await deleteThumbsById(r.id);
|
||||
await deleteFromDB(r.id, user);
|
||||
}
|
||||
|
||||
log(`🔴 [DEL_DIR] Cartella eliminata. Totale file=${rows.length}`);
|
||||
|
||||
return { deleted: rows.length };
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {
|
||||
handleAddFile,
|
||||
handleDeleteFile,
|
||||
handleAddDir,
|
||||
handleDeleteDir
|
||||
};
|
||||
|
|
@ -1,64 +1,61 @@
|
|||
// api_v1/scanner/scanCartella.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const { sha256 } = require('./utils');
|
||||
const { SUPPORTED_EXTS } = require('../config');
|
||||
|
||||
/**
|
||||
* scanCartella: genera informazioni sui file nella cartella
|
||||
* Non esegue processFile né scrive sul DB: lascia la logica di confronto a scanPhoto.
|
||||
*
|
||||
* Parametri:
|
||||
* - userName
|
||||
* - cartella
|
||||
* - absCartella
|
||||
* - db (opzionale, mantenuto per compatibilità)
|
||||
*/
|
||||
async function* scanCartella(userName, cartella, absCartella, db) {
|
||||
|
||||
async function* walk(currentAbs, relPath = '') {
|
||||
let entries = [];
|
||||
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;
|
||||
|
||||
// ID deterministico (stesso criterio di prima)
|
||||
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
||||
|
||||
// Stat veloce
|
||||
let st;
|
||||
try { st = await fsp.stat(absPath); } catch { continue; }
|
||||
|
||||
yield {
|
||||
id,
|
||||
user: userName,
|
||||
cartella,
|
||||
name: e.name,
|
||||
relPath: fileRelPath,
|
||||
absPath,
|
||||
ext,
|
||||
stat: st,
|
||||
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
yield* walk(absCartella);
|
||||
}
|
||||
|
||||
module.exports = scanCartella;
|
||||
// api_v1/scanner/scanCartella.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const fs = require('fs');
|
||||
const { sha256 } = require('./utils');
|
||||
const { SUPPORTED_EXTS } = require('../config');
|
||||
const { log } = require('./logger');
|
||||
|
||||
async function* scanCartella(userName, cartella, absCartella, db) {
|
||||
|
||||
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;
|
||||
|
||||
// ID deterministico basato sul percorso (come l’originale)
|
||||
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
||||
|
||||
let st;
|
||||
try {
|
||||
st = await fsp.stat(absPath);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
log(`📂 scanCartella → user=${userName} cartella=${cartella} relPath=${fileRelPath} id=${id}`);
|
||||
|
||||
yield {
|
||||
id,
|
||||
user: userName,
|
||||
cartella,
|
||||
name: e.name,
|
||||
relPath: fileRelPath,
|
||||
absPath,
|
||||
ext,
|
||||
stat: st,
|
||||
path: `/photos/${userName}/original/${cartella}/${fileRelPath}`
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
yield* walk(absCartella);
|
||||
}
|
||||
|
||||
module.exports = scanCartella;
|
||||
|
|
|
|||
233
api_v1/scanner/scanFile.js
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
// api_v1/scanner/scanFile.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const { sha256 } = require('./utils');
|
||||
const { SUPPORTED_EXTS } = require('../config');
|
||||
const { log } = require('./logger');
|
||||
|
||||
/**
|
||||
* scanFile: genera informazioni su UN singolo file
|
||||
* Restituisce lo stesso formato di scanCartella, ma senza recursion.
|
||||
*
|
||||
* Parametri:
|
||||
* - userName
|
||||
* - cartella
|
||||
* - absFile (percorso assoluto del file)
|
||||
* - db (opzionale)
|
||||
*/
|
||||
async function scanFileEntry(userName, cartella, absFile, db) {
|
||||
const ext = path.extname(absFile).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) {
|
||||
log(`⛔ Estensione non supportata: ${ext}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = path.basename(absFile);
|
||||
const relPath = name; // singolo file → niente struttura ricorsiva
|
||||
|
||||
let st;
|
||||
try {
|
||||
st = await fsp.stat(absFile);
|
||||
} catch {
|
||||
log(`⛔ Impossibile leggere stat per ${absFile}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = sha256(`${userName}/${cartella}/${relPath}`);
|
||||
|
||||
log(`📄 scanFile → user=${userName} cartella=${cartella} file=${name} id=${id}`);
|
||||
|
||||
return {
|
||||
id,
|
||||
user: userName,
|
||||
cartella,
|
||||
name,
|
||||
relPath,
|
||||
absPath: absFile,
|
||||
ext,
|
||||
stat: st,
|
||||
path: `/photos/${userName}/original/${cartella}/${relPath}`
|
||||
};
|
||||
}
|
||||
|
||||
async function scanFile(userName, cartella, absFile, db) {
|
||||
const f = await scanFile(user, cart, absFile);
|
||||
const fileName = f.name;
|
||||
const id = f.id;
|
||||
const st = f.stat;
|
||||
|
||||
|
||||
let prev = await db("photos")
|
||||
.select("id", "size_bytes", "mtimeMs", "_indexHash", "fast_hash", "path")
|
||||
.where({ id })
|
||||
.first();
|
||||
|
||||
if (prev && LOG_VERBOSE) {
|
||||
log(`${prefix} ⚪ Invariato: ${fileName}`);
|
||||
}
|
||||
|
||||
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// PATH CAMBIATO = NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (prev && prev.path !== f.path) {
|
||||
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||
prev = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (!prev) {
|
||||
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
// INSERT CORRETTO (senza colonna gps)
|
||||
await db("photos").insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'added',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
||||
newFiles.push(meta);
|
||||
//log(`🟢 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-SIZE-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.size_bytes === st.size) {
|
||||
//log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-HASH-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.fast_hash === fastHash) {
|
||||
//log(`🔵 [FAST-HASH-SKIP] id=${id}`);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MODIFICATO
|
||||
// ---------------------------------------------------------
|
||||
//log(`🟠 [FULL-SCAN] id=${id}`);
|
||||
log(`${prefix} 🟠 Nuovo/Modificato: ${fileName}`);
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
await db("photos")
|
||||
.insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
})
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'updated',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
newFiles.push(meta);
|
||||
log(`${prefix} 🟠 Nuovo/Modificato al server ${fileName}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
module.exports = scanFile;
|
||||
53
api_v1/scanner/scanFile1.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// api_v1/scanner/scanFile.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const { sha256 } = require('./utils');
|
||||
const { SUPPORTED_EXTS } = require('../config');
|
||||
const { log } = require('./logger');
|
||||
|
||||
/**
|
||||
* scanFile: genera informazioni su UN singolo file
|
||||
* Restituisce lo stesso formato di scanCartella, ma senza recursion.
|
||||
*
|
||||
* Parametri:
|
||||
* - userName
|
||||
* - cartella
|
||||
* - absFile (percorso assoluto del file)
|
||||
* - db (opzionale)
|
||||
*/
|
||||
async function scanFile(userName, cartella, absFile, db) {
|
||||
const ext = path.extname(absFile).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) {
|
||||
log(`⛔ Estensione non supportata: ${ext}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = path.basename(absFile);
|
||||
const relPath = name; // singolo file → niente struttura ricorsiva
|
||||
|
||||
let st;
|
||||
try {
|
||||
st = await fsp.stat(absFile);
|
||||
} catch {
|
||||
log(`⛔ Impossibile leggere stat per ${absFile}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const id = sha256(`${userName}/${cartella}/${relPath}`);
|
||||
|
||||
log(`📄 scanFile → user=${userName} cartella=${cartella} file=${name} id=${id}`);
|
||||
|
||||
return {
|
||||
id,
|
||||
user: userName,
|
||||
cartella,
|
||||
name,
|
||||
relPath,
|
||||
absPath: absFile,
|
||||
ext,
|
||||
stat: st,
|
||||
path: `/photos/${userName}/original/${cartella}/${relPath}`
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = scanFile;
|
||||
55
api_v1/scanner/scanFileEntry.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// api_v1/scanner/scanFileEntry.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const { sha256 } = require('./utils');
|
||||
const { SUPPORTED_EXTS } = require('../config');
|
||||
const { log } = require('./logger');
|
||||
|
||||
/**
|
||||
* scanFile: genera informazioni su UN singolo file
|
||||
* Restituisce lo stesso formato di scanCartella, ma senza recursion.
|
||||
*
|
||||
* Parametri:
|
||||
* - userName
|
||||
* - cartella
|
||||
* - absFile (percorso assoluto del file)
|
||||
* - db (opzionale)
|
||||
*/
|
||||
async function scanFile(userName, cartella, absFile, db) {
|
||||
const ext = path.extname(absFile).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) {
|
||||
log(`⛔ Estensione non supportata: ${ext}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = path.basename(absFile);
|
||||
const relPath = name; // singolo file → niente struttura ricorsiva
|
||||
|
||||
// ID deterministico (funziona anche se il file non esiste)
|
||||
const id = sha256(`${userName}/${cartella}/${relPath}`);
|
||||
|
||||
let st = null;
|
||||
try {
|
||||
st = await fsp.stat(absFile);
|
||||
} catch {
|
||||
// File NON esiste → caso DEL
|
||||
log(`⛔ Impossibile leggere stat per ${absFile} (file mancante)`);
|
||||
// Continuiamo comunque: l'ID è valido e serve per DEL
|
||||
}
|
||||
|
||||
log(`📄 scanFile → user=${userName} cartella=${cartella} file=${name} id=${id}`);
|
||||
|
||||
return {
|
||||
id,
|
||||
user: userName,
|
||||
cartella,
|
||||
name,
|
||||
relPath,
|
||||
absPath: absFile,
|
||||
ext,
|
||||
stat: st, // può essere null
|
||||
path: `/photos/${userName}/original/${cartella}/${relPath}`
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = scanFile;
|
||||
75
api_v1/scanner/scanNewCartella.js
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
// api_v1/scanner/scanNewCartella.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const { WEB_ROOT, SUPPORTED_EXTS } = require('../config');
|
||||
const scanPhoto = require('./scanPhoto');
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Conta ricorsivamente i file in /photos/<user>/original/<folder>/
|
||||
// ---------------------------------------------------------
|
||||
async function countFilesInUserFolder(userName, folderName) {
|
||||
const rootDir = path.join(WEB_ROOT, userName, "original", folderName);
|
||||
let count = 0;
|
||||
|
||||
async function walk(dir) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
const abs = path.join(dir, e.name);
|
||||
|
||||
if (e.isDirectory()) {
|
||||
await walk(abs);
|
||||
} else {
|
||||
const ext = path.extname(e.name).toLowerCase();
|
||||
if (SUPPORTED_EXTS.has(ext)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(rootDir);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// Crea thumbs/<folder> e scansiona la cartella come fa /scan
|
||||
// ---------------------------------------------------------
|
||||
async function scanNewCartella(userName, folderName, db) {
|
||||
// 1) crea la cartella root in thumbs
|
||||
const thumbsDir = path.join(WEB_ROOT, userName, "thumbs", folderName);
|
||||
await fsp.mkdir(thumbsDir, { recursive: true });
|
||||
|
||||
// 2) conta i file ricorsivamente
|
||||
const TOTAL_FILES = await countFilesInUserFolder(userName, folderName);
|
||||
|
||||
// 3) esegui lo scan della cartella
|
||||
const CURRENT = { value: 0 };
|
||||
const start = Date.now();
|
||||
|
||||
const newFiles = await scanPhoto(
|
||||
folderName,
|
||||
userName,
|
||||
db,
|
||||
CURRENT,
|
||||
TOTAL_FILES,
|
||||
start
|
||||
);
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
folder: folderName,
|
||||
totalFiles: TOTAL_FILES,
|
||||
newFiles
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
scanNewCartella,
|
||||
countFilesInUserFolder
|
||||
};
|
||||
|
|
@ -3,22 +3,19 @@ const path = require('path');
|
|||
const fsp = require('fs/promises');
|
||||
|
||||
const { log } = require('./logger');
|
||||
const scanCartella = require('./scanCartella');
|
||||
const processFile = require('./processFile');
|
||||
const scanPhotoCartella = require('./scanPhotoCartella');
|
||||
const postWithAuth = require('./postWithAuth');
|
||||
const { sha256 } = require('./utils');
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
|
||||
const {
|
||||
WEB_ROOT,
|
||||
SEND_PHOTOS,
|
||||
BASE_URL,
|
||||
SUPPORTED_EXTS
|
||||
BASE_URL
|
||||
} = require('../config');
|
||||
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
|
||||
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FORMAT TIME
|
||||
// ---------------------------------------------------------
|
||||
function formatTime(ms) {
|
||||
const sec = Math.floor(ms / 1000);
|
||||
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
||||
|
|
@ -27,293 +24,56 @@ function formatTime(ms) {
|
|||
return `${h}:${m}:${s}`;
|
||||
}
|
||||
|
||||
async function countFilesFast(rootDir) {
|
||||
let count = 0;
|
||||
// ---------------------------------------------------------
|
||||
// UPDATE STATUS FILE
|
||||
// ---------------------------------------------------------
|
||||
async function updateStatusFile(CURRENT, TOTAL_FILES, start) {
|
||||
const now = Date.now();
|
||||
const elapsedMs = now - start;
|
||||
const avg = elapsedMs / Math.max(CURRENT.value, 1);
|
||||
const remainingMs = (TOTAL_FILES - CURRENT.value) * avg;
|
||||
|
||||
async function walk(dir) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const status = {
|
||||
current: CURRENT.value,
|
||||
total: TOTAL_FILES,
|
||||
percent: Number((CURRENT.value / TOTAL_FILES * 100).toFixed(2)),
|
||||
eta: formatTime(remainingMs),
|
||||
elapsed: formatTime(elapsedMs)
|
||||
};
|
||||
|
||||
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;
|
||||
const statusPath = path.resolve(__dirname, "..", "..", "public/photos/scan_status.json");
|
||||
await fsp.writeFile(statusPath, JSON.stringify(status));
|
||||
}
|
||||
|
||||
async function scanPhoto(dir, userName, db) {
|
||||
const start = Date.now();
|
||||
log(`🔵 Inizio scan globale per user=${userName}`);
|
||||
// ---------------------------------------------------------
|
||||
// SCAN UNA SOLA CARTELLA
|
||||
// ---------------------------------------------------------
|
||||
async function scanPhoto(dir, userName, db, CURRENT, TOTAL_FILES, start) {
|
||||
const newFiles = [];
|
||||
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||
const userDir = path.join(photosRoot, userName, 'original');
|
||||
|
||||
let newFiles = [];
|
||||
const cartella = dir;
|
||||
const absCartella = path.join(userDir, cartella);
|
||||
|
||||
const TOTAL_FILES = await countFilesFast(userDir);
|
||||
if (TOTAL_FILES === 0) {
|
||||
log(`Nessun file trovato per user=${userName}`);
|
||||
return [];
|
||||
}
|
||||
log(`📁 [SCAN CARTELLA] ${cartella}`);
|
||||
|
||||
log(`📦 File totali da processare: ${TOTAL_FILES}`);
|
||||
|
||||
let CURRENT = 0;
|
||||
|
||||
async function updateStatusFile() {
|
||||
const now = Date.now();
|
||||
const elapsedMs = now - start;
|
||||
const avg = elapsedMs / Math.max(CURRENT, 1);
|
||||
const remainingMs = (TOTAL_FILES - CURRENT) * avg;
|
||||
|
||||
const status = {
|
||||
current: CURRENT,
|
||||
total: TOTAL_FILES,
|
||||
percent: Number((CURRENT / TOTAL_FILES * 100).toFixed(2)),
|
||||
eta: formatTime(remainingMs),
|
||||
elapsed: formatTime(elapsedMs)
|
||||
};
|
||||
|
||||
const statusPath = path.resolve(__dirname, "..", "..", "public/photos/scan_status.json");
|
||||
await fsp.writeFile(statusPath, JSON.stringify(status));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SCAN DI UNA SINGOLA CARTELLA
|
||||
// ---------------------------------------------------------
|
||||
async function scanSingleFolder(user, cartella, absCartella) {
|
||||
log(`📁 Inizio cartella: ${cartella}`);
|
||||
|
||||
const rows = await db('photos')
|
||||
.where({ user, cartella })
|
||||
.select('id');
|
||||
|
||||
const idsSet = new Set(rows.map(r => r.id));
|
||||
|
||||
for await (const f of scanCartella(user, cartella, absCartella, db)) {
|
||||
CURRENT++;
|
||||
await updateStatusFile();
|
||||
const prefix = `[${CURRENT}/${TOTAL_FILES}]`;
|
||||
const fileName = f.name;
|
||||
const id = f.id;
|
||||
const st = f.stat;
|
||||
|
||||
|
||||
let prev = await db("photos")
|
||||
.select("id", "size_bytes", "mtimeMs", "_indexHash", "fast_hash", "path")
|
||||
.where({ id })
|
||||
.first();
|
||||
|
||||
if (prev && LOG_VERBOSE) {
|
||||
log(`${prefix} ⚪ Invariato: ${fileName}`);
|
||||
}
|
||||
|
||||
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// PATH CAMBIATO = NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (prev && prev.path !== f.path) {
|
||||
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||
prev = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (!prev) {
|
||||
log(`${prefix} 🟢 Nuovo/Modificato: ${fileName}`);
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
// INSERT CORRETTO (senza colonna gps)
|
||||
await db("photos").insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'added',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
idsSet.delete(id);
|
||||
|
||||
newFiles.push(meta);
|
||||
//log(`🟢 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-SIZE-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.size_bytes === st.size) {
|
||||
//log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
|
||||
idsSet.delete(id);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-HASH-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.fast_hash === fastHash) {
|
||||
//log(`🔵 [FAST-HASH-SKIP] id=${id}`);
|
||||
idsSet.delete(id);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MODIFICATO
|
||||
// ---------------------------------------------------------
|
||||
//log(`🟠 [FULL-SCAN] id=${id}`);
|
||||
log(`${prefix} 🟠 Nuovo/Modificato: ${fileName}`);
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
await db("photos")
|
||||
.insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
})
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'updated',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
idsSet.delete(id);
|
||||
newFiles.push(meta);
|
||||
log(`${prefix} 🟠 Nuovo/Modificato al server ${fileName}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ORFANI
|
||||
// ---------------------------------------------------------
|
||||
for (const orphanId of idsSet) {
|
||||
log(` 🔴 Cancellato ${orphanId}`);
|
||||
await deleteThumbsById(orphanId);
|
||||
await deleteFromDB(orphanId, user);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SCAN DI TUTTE LE CARTELLE
|
||||
// ---------------------------------------------------------
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(userDir, { withFileTypes: true });
|
||||
} catch {
|
||||
log(`Nessuna directory per utente ${userName}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory()) continue;
|
||||
const cartella = e.name;
|
||||
const absCartella = path.join(userDir, cartella);
|
||||
await scanSingleFolder(userName, cartella, absCartella);
|
||||
}
|
||||
await scanPhotoCartella(
|
||||
db,
|
||||
userName,
|
||||
cartella,
|
||||
absCartella,
|
||||
newFiles,
|
||||
updateStatusFile,
|
||||
CURRENT,
|
||||
TOTAL_FILES,
|
||||
start,
|
||||
deleteThumbsById,
|
||||
deleteFromDB
|
||||
);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// INVIO AL SERVER REMOTO
|
||||
|
|
@ -322,21 +82,18 @@ async function scanPhoto(dir, userName, db) {
|
|||
log(`📤 [SEND START] newFiles=${newFiles.length}`);
|
||||
|
||||
for (const p of newFiles) {
|
||||
|
||||
try {
|
||||
p.user = userName;
|
||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||
log(`📥 [SENT TO SERVER] ${p.name}`);
|
||||
} catch (err) {
|
||||
log(`❌ [SERVER SENT ERROR] ${p.name}→ ${err.message}`);
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,349 +0,0 @@
|
|||
// api_v1/scanner/scanPhoto.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
|
||||
const { log } = require('./logger');
|
||||
const scanCartella = require('./scanCartella');
|
||||
const processFile = require('./processFile');
|
||||
const postWithAuth = require('./postWithAuth');
|
||||
const { sha256 } = require('./utils');
|
||||
|
||||
const {
|
||||
WEB_ROOT,
|
||||
SEND_PHOTOS,
|
||||
BASE_URL,
|
||||
SUPPORTED_EXTS
|
||||
} = require('../config');
|
||||
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
|
||||
const LOG_VERBOSE = (process.env.LOG_VERBOSE === "true");
|
||||
|
||||
function formatTime(ms) {
|
||||
const sec = Math.floor(ms / 1000);
|
||||
const h = String(Math.floor(sec / 3600)).padStart(2, '0');
|
||||
const m = String(Math.floor((sec % 3600) / 60)).padStart(2, '0');
|
||||
const s = String(sec % 60).padStart(2, '0');
|
||||
return `${h}:${m}:${s}`;
|
||||
}
|
||||
|
||||
async function countFilesFast(rootDir) {
|
||||
let count = 0;
|
||||
|
||||
async function walk(dir) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
const abs = path.join(dir, e.name);
|
||||
|
||||
if (e.isDirectory()) {
|
||||
await walk(abs);
|
||||
} else {
|
||||
const ext = path.extname(e.name).toLowerCase();
|
||||
if (SUPPORTED_EXTS.has(ext)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(rootDir);
|
||||
return count;
|
||||
}
|
||||
|
||||
async function scanPhoto(dir, userName, db) {
|
||||
const start = Date.now();
|
||||
log(`🔵 Inizio scan globale per user=${userName}`);
|
||||
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||
const userDir = path.join(photosRoot, userName, 'original');
|
||||
|
||||
let newFiles = [];
|
||||
|
||||
const TOTAL_FILES = await countFilesFast(userDir);
|
||||
if (TOTAL_FILES === 0) {
|
||||
log(`Nessun file trovato per user=${userName}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
log(`📦 File totali da processare: ${TOTAL_FILES}`);
|
||||
|
||||
let CURRENT = 0;
|
||||
|
||||
async function updateStatusFile() {
|
||||
const now = Date.now();
|
||||
const elapsedMs = now - start;
|
||||
const avg = elapsedMs / Math.max(CURRENT, 1);
|
||||
const remainingMs = (TOTAL_FILES - CURRENT) * avg;
|
||||
|
||||
const status = {
|
||||
current: CURRENT,
|
||||
total: TOTAL_FILES,
|
||||
percent: Number((CURRENT / TOTAL_FILES * 100).toFixed(2)),
|
||||
eta: formatTime(remainingMs),
|
||||
elapsed: formatTime(elapsedMs)
|
||||
};
|
||||
|
||||
const statusPath = path.resolve(__dirname, "..", "..", "public/photos/scan_status.json");
|
||||
await fsp.writeFile(statusPath, JSON.stringify(status));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SCAN DI UNA SINGOLA CARTELLA
|
||||
// ---------------------------------------------------------
|
||||
async function scanSingleFolder(user, cartella, absCartella) {
|
||||
log(`📁 Inizio cartella: ${cartella}`);
|
||||
|
||||
const rows = await db('photos')
|
||||
.where({ user, cartella })
|
||||
.select('id');
|
||||
|
||||
const idsSet = new Set(rows.map(r => r.id));
|
||||
|
||||
for await (const f of scanCartella(user, cartella, absCartella, db)) {
|
||||
CURRENT++;
|
||||
await updateStatusFile();
|
||||
|
||||
const fileName = f.name;
|
||||
const id = f.id;
|
||||
const st = f.stat;
|
||||
|
||||
log(`🔍 [SCAN] File: ${fileName} id=${id} size=${st.size} mtime=${st.mtimeMs}`);
|
||||
|
||||
let prev = await db("photos")
|
||||
.select("id", "size_bytes", "mtimeMs", "_indexHash", "fast_hash", "path")
|
||||
.where({ id })
|
||||
.first();
|
||||
|
||||
if (prev) {
|
||||
log(` ↳ [PREV FOUND] size=${prev.size_bytes} mtime=${prev.mtimeMs} path=${prev.path}`);
|
||||
} else {
|
||||
log(` ↳ [PREV NOT FOUND]`);
|
||||
}
|
||||
|
||||
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// PATH CAMBIATO = NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (prev && prev.path !== f.path) {
|
||||
log(`🟡 [PATH CHANGED] id=${id} old=${prev.path} new=${f.path}`);
|
||||
prev = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (!prev) {
|
||||
log(`🟢 [NEW FILE DETECTED] id=${id} name=${fileName}`);
|
||||
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
// INSERT CORRETTO (senza colonna gps)
|
||||
await db("photos").insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'added',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
idsSet.delete(id);
|
||||
|
||||
newFiles.push(meta);
|
||||
log(`🟢 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-SIZE-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.size_bytes === st.size) {
|
||||
log(`🔵 [FAST-SIZE-SKIP] id=${id}`);
|
||||
idsSet.delete(id);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FAST-HASH-SKIP
|
||||
// ---------------------------------------------------------
|
||||
if (prev.fast_hash === fastHash) {
|
||||
log(`🔵 [FAST-HASH-SKIP] id=${id}`);
|
||||
idsSet.delete(id);
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MODIFICATO
|
||||
// ---------------------------------------------------------
|
||||
log(`🟠 [FULL-SCAN] id=${id}`);
|
||||
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
await db("photos")
|
||||
.insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
_indexHash: meta._indexHash,
|
||||
fast_hash: fastHash
|
||||
})
|
||||
.onConflict("id")
|
||||
.merge();
|
||||
|
||||
await db('photo_changes').insert({
|
||||
photo_id: meta.id,
|
||||
user,
|
||||
change_type: 'updated',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
idsSet.delete(id);
|
||||
newFiles.push(meta);
|
||||
log(`🟠 [PUSH newFiles] id=${meta.id} path=${meta.path}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// ORFANI
|
||||
// ---------------------------------------------------------
|
||||
for (const orphanId of idsSet) {
|
||||
log(`🔴 [ORPHAN DELETE] id=${orphanId}`);
|
||||
|
||||
await deleteThumbsById(orphanId);
|
||||
await deleteFromDB(orphanId, user);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SCAN DI TUTTE LE CARTELLE
|
||||
// ---------------------------------------------------------
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(userDir, { withFileTypes: true });
|
||||
} catch {
|
||||
log(`Nessuna directory per utente ${userName}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory()) continue;
|
||||
const cartella = e.name;
|
||||
const absCartella = path.join(userDir, cartella);
|
||||
await scanSingleFolder(userName, cartella, absCartella);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// INVIO AL SERVER REMOTO
|
||||
// ---------------------------------------------------------
|
||||
if (SEND_PHOTOS && BASE_URL && newFiles.length > 0) {
|
||||
log(`📤 [SEND START] newFiles=${newFiles.length}`);
|
||||
|
||||
for (const p of newFiles) {
|
||||
log(`📤 [SEND] id=${p.id} name=${p.name} path=${p.path}`);
|
||||
|
||||
try {
|
||||
await postWithAuth(`${BASE_URL}/photos`, p);
|
||||
log(`📥 [SERVER RESPONSE OK] id=${p.id}`);
|
||||
} catch (err) {
|
||||
log(`❌ [SERVER ERROR] id=${p.id} → ${err.message}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log(`⚠️ [NO SEND] newFiles=${newFiles.length}`);
|
||||
}
|
||||
|
||||
const elapsed = ((Date.now() - start) / 1000).toFixed(2);
|
||||
log(`🟣 Scan COMPLETATO in ${elapsed}s`);
|
||||
|
||||
return newFiles;
|
||||
}
|
||||
|
||||
module.exports = scanPhoto;
|
||||
57
api_v1/scanner/scanPhotoCartella.js
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
// api_v1/scanner/scanPhotoCartella.js
|
||||
const scanCartella = require('./scanCartella');
|
||||
const scanPhotoSingle = require('./scanPhotoSingle');
|
||||
const { log } = require('./logger');
|
||||
const recordChange = require('./recordChange'); // 👈 AGGIUNTO
|
||||
|
||||
async function scanPhotoCartella(
|
||||
db,
|
||||
user,
|
||||
cartella,
|
||||
absCartella,
|
||||
newFiles,
|
||||
updateStatusFile,
|
||||
CURRENT,
|
||||
TOTAL_FILES,
|
||||
start,
|
||||
deleteThumbsById,
|
||||
deleteFromDB
|
||||
) {
|
||||
log(`📁 Scan cartella: ${cartella}`);
|
||||
|
||||
// Recupera gli ID già presenti nel DB
|
||||
const rows = await db('photos')
|
||||
.where({ user, cartella })
|
||||
.select('id');
|
||||
|
||||
const idsSet = new Set(rows.map(r => r.id));
|
||||
|
||||
// Scansiona i file della cartella
|
||||
for await (const f of scanCartella(user, cartella, absCartella, db)) {
|
||||
|
||||
// Aggiorna il contatore
|
||||
CURRENT.value++;
|
||||
|
||||
// Aggiorna stato avanzamento
|
||||
await updateStatusFile(CURRENT, TOTAL_FILES, start);
|
||||
|
||||
// Processa il file
|
||||
const meta = await scanPhotoSingle(db, user, cartella, f, newFiles);
|
||||
|
||||
// Anche se identico, NON è orfano
|
||||
idsSet.delete(f.id);
|
||||
}
|
||||
|
||||
// Gestione orfani
|
||||
for (const orphanId of idsSet) {
|
||||
log(`🔴 Orfano: ${orphanId}`);
|
||||
|
||||
await deleteThumbsById(orphanId);
|
||||
await deleteFromDB(orphanId, user);
|
||||
|
||||
// 👇 REGISTRAZIONE MODIFICA
|
||||
await recordChange(db, orphanId, user, "removed");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = scanPhotoCartella;
|
||||
141
api_v1/scanner/scanPhotoSingle.js
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// api_v1/scanner/scanPhotoSingle.js
|
||||
const processFile = require('./processFile');
|
||||
const { sha256 } = require('./utils');
|
||||
const { log } = require('./logger');
|
||||
const recordChange = require('./recordChange'); // 👈 AGGIUNTO
|
||||
|
||||
async function scanPhotoSingle(db, user, cartella, f, newFiles, prefix = '') {
|
||||
const fileName = f.name;
|
||||
const id = f.id;
|
||||
const st = f.stat;
|
||||
|
||||
// Recupero precedente SENZA contentHash
|
||||
let prev = await db("photos")
|
||||
.select("id", "size_bytes", "mtimeMs", "fast_hash", "path")
|
||||
.where({ id })
|
||||
.first();
|
||||
|
||||
// Hash veloce basato su size + mtime
|
||||
const fastHash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// PATH CAMBIATO = NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (prev && prev.path !== f.path) {
|
||||
log(`${prefix} 🟢 Nuovo/Modificato (path changed): ${fileName}`);
|
||||
prev = null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// NUOVO FILE
|
||||
// ---------------------------------------------------------
|
||||
if (!prev) {
|
||||
log(`${prefix} 🟢 Nuovo: ${fileName}`);
|
||||
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
await db("photos").insert({
|
||||
id: meta.id,
|
||||
user,
|
||||
cartella,
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
// 👇 REGISTRAZIONE MODIFICA
|
||||
await recordChange(db, id, user, "added");
|
||||
|
||||
newFiles.push(meta);
|
||||
return meta;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// IDENTICO (fastHash uguale)
|
||||
// ---------------------------------------------------------
|
||||
if (prev.fast_hash === fastHash) {
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
path: f.path,
|
||||
size_bytes: st.size,
|
||||
mtimeMs: st.mtimeMs,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MODIFICATO (fastHash diverso)
|
||||
// ---------------------------------------------------------
|
||||
log(`${prefix} 🟠 Modificato: ${fileName}`);
|
||||
|
||||
const meta = await processFile(
|
||||
user,
|
||||
cartella,
|
||||
f.relPath,
|
||||
f.absPath,
|
||||
f.ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = f.path;
|
||||
|
||||
await db("photos")
|
||||
.where({ id })
|
||||
.update({
|
||||
name: meta.name,
|
||||
path: meta.path,
|
||||
thub1: meta.thub1,
|
||||
thub2: meta.thub2,
|
||||
mime_type: meta.mime_type,
|
||||
width: meta.width,
|
||||
height: meta.height,
|
||||
rotation: meta.rotation,
|
||||
size_bytes: meta.size_bytes,
|
||||
mtimeMs: meta.mtimeMs,
|
||||
duration_ms: meta.duration_ms,
|
||||
taken_at: meta.taken_at,
|
||||
data: meta.data,
|
||||
lat: meta.gps?.lat ?? null,
|
||||
lon: meta.gps?.lng ?? null,
|
||||
alt: meta.gps?.alt ?? null,
|
||||
location: meta.location ? JSON.stringify(meta.location) : null,
|
||||
fast_hash: fastHash
|
||||
});
|
||||
|
||||
// 👇 REGISTRAZIONE MODIFICA
|
||||
await recordChange(db, id, user, "updated");
|
||||
|
||||
newFiles.push(meta);
|
||||
return meta;
|
||||
}
|
||||
|
||||
module.exports = scanPhotoSingle;
|
||||
110
api_v1/scanner/scanPhotosUser.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
// api_v1/scanner/scanPhotosUser.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const scanPhoto = require('./scanPhoto');
|
||||
const { log } = require('./logger');
|
||||
const { WEB_ROOT, SUPPORTED_EXTS } = require('../config');
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// PRIMA PASSATA: conta TUTTI i file reali (ricorsiva)
|
||||
// ---------------------------------------------------------
|
||||
async function countFilesUser(rootDir) {
|
||||
let count = 0;
|
||||
|
||||
async function walk(dir) {
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true });
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
const abs = path.join(dir, e.name);
|
||||
|
||||
if (e.isDirectory()) {
|
||||
await walk(abs);
|
||||
} else {
|
||||
const ext = path.extname(e.name).toLowerCase();
|
||||
if (SUPPORTED_EXTS.has(ext)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await walk(rootDir);
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// SECONDA PASSATA: scansiona SOLO le cartelle vere
|
||||
// ---------------------------------------------------------
|
||||
async function scanPhotosUser(userName, db) {
|
||||
log(`🔵 Inizio scan TUTTE le cartelle per user=${userName}`);
|
||||
|
||||
const photosRoot = path.resolve(__dirname, '..', '..', WEB_ROOT, 'photos');
|
||||
const userDir = path.join(photosRoot, userName, 'original');
|
||||
|
||||
let entries = [];
|
||||
try {
|
||||
entries = await fsp.readdir(userDir, { withFileTypes: true });
|
||||
} catch {
|
||||
log(`❌ Nessuna directory per utente ${userName}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 🔥 Filtra SOLO cartelle vere dentro "original"
|
||||
const folders = entries.filter(e => e.isDirectory()).map(e => e.name);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// 🔥 RIMOZIONE CARTELLE CANCELLATE DAL FILESYSTEM
|
||||
// ---------------------------------------------------------
|
||||
const createDeleteFolderFunctions = require('./deleteFolder');
|
||||
const { deleteFolderForUser } = createDeleteFolderFunctions(db);
|
||||
|
||||
const dbFolders = await db('photos')
|
||||
.where({ user: userName })
|
||||
.distinct('cartella')
|
||||
.pluck('cartella');
|
||||
|
||||
for (const dbFolder of dbFolders) {
|
||||
if (!folders.includes(dbFolder)) {
|
||||
log(`🗑️ Cartella rimossa dal filesystem: ${dbFolder}`);
|
||||
await deleteFolderForUser(userName, dbFolder);
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 PRIMA PASSATA: conta i file reali
|
||||
const TOTAL_FILES = await countFilesUser(userDir);
|
||||
const CURRENT = { value: 0 };
|
||||
const start = Date.now();
|
||||
|
||||
log(`📊 File totali da scansionare: ${TOTAL_FILES}`);
|
||||
|
||||
const allNewFiles = [];
|
||||
|
||||
// 🔥 SECONDA PASSATA: UNA SOLA SCANSIONE PER CARTELLA
|
||||
for (const cartella of folders) {
|
||||
log(`📁 Scan cartella utente: ${cartella}`);
|
||||
|
||||
const newFiles = await scanPhoto(
|
||||
cartella,
|
||||
userName,
|
||||
db,
|
||||
CURRENT,
|
||||
TOTAL_FILES,
|
||||
start
|
||||
);
|
||||
|
||||
if (newFiles?.length) {
|
||||
allNewFiles.push(...newFiles);
|
||||
}
|
||||
}
|
||||
|
||||
log(`🟣 Scan COMPLETATO per user=${userName}. Nuovi file totali: ${allNewFiles.length}`);
|
||||
|
||||
return allNewFiles;
|
||||
}
|
||||
|
||||
module.exports = scanPhotosUser;
|
||||
|
|
@ -1,66 +1,44 @@
|
|||
// scanner/scanUser.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const scanCartella = require('./scanCartella');
|
||||
const processFile = require('./processFile');
|
||||
const { sha256 } = require('./utils');
|
||||
const { SUPPORTED_EXTS } = require('../config');
|
||||
|
||||
async function scanUserRoot(userName, userDir, previousIndexTree) {
|
||||
console.log(`\n🔵 Inizio scan user: ${userName}`);
|
||||
|
||||
const results = [];
|
||||
const entries = await fsp.readdir(userDir, { withFileTypes: true });
|
||||
|
||||
// ROOT FILES
|
||||
for (const e of entries) {
|
||||
if (e.isDirectory()) continue;
|
||||
|
||||
const ext = path.extname(e.name).toLowerCase();
|
||||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||
|
||||
const absPath = path.join(userDir, e.name);
|
||||
const st = await fsp.stat(absPath);
|
||||
|
||||
const fileRelPath = e.name;
|
||||
const cartella = '_root';
|
||||
const id = sha256(`${userName}/${cartella}/${fileRelPath}`);
|
||||
|
||||
const hash = sha256(`${st.size}-${st.mtimeMs}`);
|
||||
const prev = previousIndexTree?.[userName]?.[cartella]?.[id];
|
||||
|
||||
if (prev && prev.hash === hash) continue;
|
||||
|
||||
const meta = await processFile(
|
||||
userName,
|
||||
cartella,
|
||||
fileRelPath,
|
||||
absPath,
|
||||
ext,
|
||||
st
|
||||
);
|
||||
|
||||
meta.id = id;
|
||||
meta.path = `/photos/${userName}/original/${cartella}/${fileRelPath}`;
|
||||
meta._indexHash = hash;
|
||||
|
||||
results.push(meta);
|
||||
}
|
||||
|
||||
// SUBFOLDERS
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory()) continue;
|
||||
|
||||
const cartella = e.name;
|
||||
const absCartella = path.join(userDir, cartella);
|
||||
|
||||
console.log(` 📁 Cartella: ${cartella}`);
|
||||
|
||||
const files = await scanCartella(userName, cartella, absCartella, previousIndexTree);
|
||||
results.push(...files);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = scanUserRoot;
|
||||
// scanner/scanUser.js
|
||||
const path = require('path');
|
||||
const fsp = require('fs/promises');
|
||||
const scanCartella = require('./scanCartella');
|
||||
|
||||
async function scanUserRoot(userName, userDir, previousIndexTree) {
|
||||
console.log(`\n🔵 Inizio scan user: ${userName}`);
|
||||
|
||||
const results = [];
|
||||
|
||||
// 🔥 SCANSIONA SOLO LA CARTELLA "original"
|
||||
const originalDir = path.join(userDir, "original");
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fsp.readdir(originalDir, { withFileTypes: true });
|
||||
} catch (err) {
|
||||
console.error(`❌ Errore lettura originalDir: ${originalDir}`, err);
|
||||
return results;
|
||||
}
|
||||
|
||||
// 🔥 SCANSIONA SOLO LE SOTTOCARTELLE DI "original"
|
||||
for (const e of entries) {
|
||||
if (!e.isDirectory()) continue;
|
||||
|
||||
const cartella = e.name;
|
||||
const absCartella = path.join(originalDir, cartella);
|
||||
|
||||
console.log(` 📁 Cartella: ${cartella}`);
|
||||
|
||||
const files = await scanCartella(
|
||||
userName,
|
||||
cartella,
|
||||
absCartella,
|
||||
previousIndexTree
|
||||
);
|
||||
|
||||
results.push(...files);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
module.exports = scanUserRoot;
|
||||
177
db/init.js
|
|
@ -1,86 +1,91 @@
|
|||
// 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();
|
||||
// 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 (senza contentHash)");
|
||||
|
||||
} 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");
|
||||
}
|
||||
|
||||
// ❌ NON aggiungiamo più contentHash
|
||||
// Se esiste già, non dà problemi.
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// 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();
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@
|
|||
const db = require('./knex');
|
||||
|
||||
async function init() {
|
||||
const exists = await db.schema.hasTable('photos');
|
||||
// -------------------------------
|
||||
// 1) Tabella PHOTOS
|
||||
// -------------------------------
|
||||
const existsPhotos = await db.schema.hasTable('photos');
|
||||
|
||||
if (!exists) {
|
||||
if (!existsPhotos) {
|
||||
await db.schema.createTable('photos', (t) => {
|
||||
t.string('id').primary();
|
||||
t.string('user');
|
||||
|
|
@ -27,13 +30,54 @@ async function init() {
|
|||
t.float('alt');
|
||||
t.text('location'); // JSON string
|
||||
|
||||
// 👉 NUOVO: hash identico al vecchio index.json
|
||||
// Hash lento (metadati)
|
||||
t.string('_indexHash');
|
||||
|
||||
// Hash veloce (size-mtime)
|
||||
t.string('fast_hash');
|
||||
});
|
||||
|
||||
console.log("✔ Tabella 'photos' creata correttamente (con _indexHash)");
|
||||
console.log("✔ Tabella 'photos' creata correttamente (con _indexHash + fast_hash)");
|
||||
|
||||
} else {
|
||||
console.log("✔ Tabella 'photos' già esistente");
|
||||
|
||||
// Controllo colonna _indexHash
|
||||
const hasIndexHash = await db.schema.hasColumn('photos', '_indexHash');
|
||||
if (!hasIndexHash) {
|
||||
await db.schema.alterTable('photos', (t) => {
|
||||
t.string('_indexHash');
|
||||
});
|
||||
console.log("✔ Colonna '_indexHash' aggiunta");
|
||||
}
|
||||
|
||||
// Controllo colonna fast_hash
|
||||
const hasFastHash = await db.schema.hasColumn('photos', 'fast_hash');
|
||||
if (!hasFastHash) {
|
||||
await db.schema.alterTable('photos', (t) => {
|
||||
t.string('fast_hash');
|
||||
});
|
||||
console.log("✔ Colonna 'fast_hash' aggiunta");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// 2) Tabella PHOTO_CHANGES
|
||||
// -------------------------------
|
||||
const existsChanges = await db.schema.hasTable('photo_changes');
|
||||
|
||||
if (!existsChanges) {
|
||||
await db.schema.createTable('photo_changes', (t) => {
|
||||
t.increments('id').primary();
|
||||
t.string('photo_id');
|
||||
t.string('user');
|
||||
t.string('change_type'); // added | updated | removed
|
||||
t.string('timestamp'); // ISO string
|
||||
});
|
||||
|
||||
console.log("✔ Tabella 'photo_changes' creata correttamente");
|
||||
} else {
|
||||
console.log("✔ Tabella 'photo_changes' già esistente");
|
||||
}
|
||||
|
||||
process.exit();
|
||||
|
|
|
|||
10
f.sh
|
|
@ -1,10 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
WATCH_DIR="./public/photos/Fabio/original"
|
||||
|
||||
echo "Monitoro: $WATCH_DIR"
|
||||
|
||||
inotifywait -m -r -e create,modify,delete "$WATCH_DIR" |
|
||||
while read path action file; do
|
||||
echo "modificato Fabio → $action su $file"
|
||||
done
|
||||
27
f1.sh
|
|
@ -1,27 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
WATCH_DIR="./public/photos/Fabio/original"
|
||||
DELAY=10 # secondi di quiete richiesti
|
||||
TIMER_PID=""
|
||||
|
||||
echo "Monitoro: $WATCH_DIR"
|
||||
|
||||
# Funzione che parte dopo 10 secondi di quiete
|
||||
run_after_delay() {
|
||||
sleep $DELAY
|
||||
echo "modificato Fabio (nessuna attività per $DELAY secondi)"
|
||||
}
|
||||
|
||||
inotifywait -m -r -e create,modify,delete "$WATCH_DIR" |
|
||||
while read path action file; do
|
||||
echo "Evento rilevato: $action su $file"
|
||||
|
||||
# Se esiste un timer precedente, lo uccidiamo
|
||||
if [[ -n "$TIMER_PID" ]]; then
|
||||
kill "$TIMER_PID" 2>/dev/null
|
||||
fi
|
||||
|
||||
# Avviamo un nuovo timer in background
|
||||
run_after_delay &
|
||||
TIMER_PID=$!
|
||||
done
|
||||
53
f2.sh
|
|
@ -1,53 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
WATCH_DIR="./public/photos/Fabio/original"
|
||||
DELAY=10
|
||||
LOCK_FILE="./Fabio.scan"
|
||||
|
||||
echo "Monitoro: $WATCH_DIR"
|
||||
|
||||
LAST_EVENT_TIME=0
|
||||
|
||||
# Thread che controlla l'inattività
|
||||
check_inactivity() {
|
||||
while true; do
|
||||
sleep 1
|
||||
|
||||
# Se non è mai arrivato un evento, continua ad aspettare
|
||||
if (( LAST_EVENT_TIME == 0 )); then
|
||||
continue
|
||||
fi
|
||||
|
||||
NOW=$(date +%s)
|
||||
DIFF=$(( NOW - LAST_EVENT_TIME ))
|
||||
|
||||
# Se non sono passati 10 secondi → continua ad aspettare
|
||||
if (( DIFF < DELAY )); then
|
||||
continue
|
||||
fi
|
||||
|
||||
# Se esiste il lock file → aspetta altri 10 secondi
|
||||
if [[ -f "$LOCK_FILE" ]]; then
|
||||
echo "Lock presente ($LOCK_FILE), attendo altri $DELAY secondi..."
|
||||
sleep $DELAY
|
||||
continue
|
||||
fi
|
||||
|
||||
# QUI: 10 secondi di quiete + lock libero → esegui
|
||||
echo "modificato Fabio (nessuna attività per $DELAY secondi)"
|
||||
touch "$LOCK_FILE"
|
||||
|
||||
# Resetta il timer per evitare ripetizioni
|
||||
LAST_EVENT_TIME=0
|
||||
done
|
||||
}
|
||||
|
||||
# Avvia il thread di controllo inattività
|
||||
check_inactivity &
|
||||
|
||||
# Listener degli eventi
|
||||
inotifywait -m -r -e create,modify,delete "$WATCH_DIR" |
|
||||
while read path action file; do
|
||||
echo "Evento rilevato: $action su $file"
|
||||
LAST_EVENT_TIME=$(date +%s)
|
||||
done
|
||||
40
find-esm.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
function scan(dir) {
|
||||
const items = fs.readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const item of items) {
|
||||
const full = path.join(dir, item.name);
|
||||
|
||||
if (item.isDirectory()) {
|
||||
scan(full);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 1) File .mjs → ESM sicuro
|
||||
if (full.endsWith(".mjs")) {
|
||||
console.log("⚠️ FILE .mjs (ESM):", full);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) File .js → controlliamo se contiene import/export
|
||||
if (full.endsWith(".js")) {
|
||||
const content = fs.readFileSync(full, "utf8");
|
||||
|
||||
if (/^\s*import\s/m.test(content)) {
|
||||
console.log("⚠️ IMPORT trovato:", full);
|
||||
}
|
||||
if (/^\s*export\s/m.test(content)) {
|
||||
console.log("⚠️ EXPORT trovato:", full);
|
||||
}
|
||||
if (/import\.meta\.url/.test(content)) {
|
||||
console.log("⚠️ import.meta.url trovato:", full);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("🔍 Scansione in corso...");
|
||||
scan(process.cwd());
|
||||
console.log("✔️ Scansione completata.");
|
||||
32
package.json
|
|
@ -1,14 +1,18 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"axios": "^1.13.6",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"exifreader": "^4.37.0",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"knex": "^3.1.0",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
{
|
||||
"type": "commonjs",
|
||||
"scripts": {
|
||||
"start": "node server.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.6",
|
||||
"bcrypt": "^6.0.0",
|
||||
"better-sqlite3": "^12.8.0",
|
||||
"dotenv": "^17.3.1",
|
||||
"exifreader": "^4.37.0",
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"knex": "^3.1.0",
|
||||
"sharp": "^0.34.5",
|
||||
"ws": "^8.19.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
<button onclick="scan()">Scansiona Foto</button>
|
||||
<button onclick="resetDB()">Reset DB</button>
|
||||
<button onclick="readDB()">Leggi DB</button>
|
||||
<button onclick="readDBUser()">Leggi DB</button>
|
||||
<button onclick="deletePhoto()">Cancella Foto per ID</button>
|
||||
<button onclick="findIdIndex()">Cerca ID in index.json</button>
|
||||
<button onclick="resetDBuser()">Reset DB Utente</button>
|
||||
|
|
@ -180,7 +180,7 @@ async function loadConfig() {
|
|||
BASE_URL = cfg.baseUrl;
|
||||
}
|
||||
|
||||
async function readDB() {
|
||||
async function readDB1() {
|
||||
const res = await fetch(`${BASE_URL}/photos`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
|
@ -189,6 +189,42 @@ async function readDB() {
|
|||
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
|
||||
}
|
||||
|
||||
async function readDBUser() {
|
||||
const payload = parseJwt(token);
|
||||
const user = payload.name;
|
||||
|
||||
const res = await fetch(`${BASE_URL}/photos?user=${encodeURIComponent(user)}`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Errore readDB:", res.status);
|
||||
return;
|
||||
}
|
||||
|
||||
db = await res.json();
|
||||
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
|
||||
}
|
||||
|
||||
async function readDB() {
|
||||
|
||||
|
||||
const res = await fetch(`${BASE_URL}/photos?user=${encodeURIComponent('Admin')}`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Errore readDB:", res.status);
|
||||
return;
|
||||
}
|
||||
|
||||
db = await res.json();
|
||||
document.getElementById("out").textContent = JSON.stringify(db, null, 2);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function resetDB() {
|
||||
await fetch(`${BASE_URL}/initDB`, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
|
|
|
|||
|
|
@ -6,10 +6,23 @@ async function apiGet(url) {
|
|||
}).then(r => r.json());
|
||||
}
|
||||
|
||||
async function getAllPhotos() {
|
||||
return apiGet(`${BASE_URL}/photos`);
|
||||
async function getAllPhotos1() {
|
||||
//const payload = parseJwt(localStorage.getItem("token"));
|
||||
//const user = payload.name || "Common";
|
||||
|
||||
//return apiGet(`${BASE_URL}/photos?user=${encodeURIComponent(user)}`);
|
||||
return apiGet(`https://prova.patachina.it/photos?user=Fabio`);
|
||||
}
|
||||
|
||||
async function getAllPhotos(user) {
|
||||
|
||||
|
||||
return apiGet(`${BASE_URL}/photos?user=${encodeURIComponent(user)}`);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function getPhotoById(id) {
|
||||
const payload = parseJwt(localStorage.getItem("token"));
|
||||
const user = payload.name || "Common";
|
||||
|
|
|
|||
|
|
@ -175,13 +175,23 @@ async function initGallery() {
|
|||
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);
|
||||
// ===============================================
|
||||
// 🛟 POLLING DI SICUREZZA (configurabile da .env)
|
||||
// ===============================================
|
||||
if (refreshSeconds > 0) {
|
||||
console.log(`[initGallery] Polling attivo ogni ${refreshSeconds} secondi...`);
|
||||
|
||||
setInterval(async () => {
|
||||
console.log(">>> TIMER: chiamata incrementalSync()");
|
||||
console.log(">>> lastSync attuale =", getLastSync());
|
||||
await incrementalSync();
|
||||
console.log(">>> incrementalSync() COMPLETATO (timer)");
|
||||
}, refreshSeconds * 1000);
|
||||
|
||||
} else {
|
||||
console.log("[initGallery] Polling disattivato (refreshSeconds = 0)");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
window.addEventListener("DOMContentLoaded", initGallery);
|
||||
|
|
|
|||
|
|
@ -171,48 +171,7 @@ document.getElementById('settingsBtn')?.addEventListener('click', () => {
|
|||
window.location.href = "admin.html";
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// LOGIN SUBMIT
|
||||
// ===============================
|
||||
/*
|
||||
document.getElementById("loginSubmit").addEventListener("click", async () => {
|
||||
const email = document.getElementById("loginEmail").value;
|
||||
const password = document.getElementById("loginPassword").value;
|
||||
const errorEl = document.getElementById("loginError");
|
||||
|
||||
errorEl.textContent = "";
|
||||
|
||||
try {
|
||||
const res = await fetch(`${window.BASE_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
errorEl.textContent = "Utente o password errati";
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
const token = data.token;
|
||||
|
||||
localStorage.setItem("token", token);
|
||||
window.token = token;
|
||||
|
||||
document.getElementById("loginModal").style.display = "none";
|
||||
|
||||
syncHeaderAuthUI();
|
||||
|
||||
// 🔥 NON caricare più foto da main.js
|
||||
// initGallery() farà tutto
|
||||
|
||||
} catch (err) {
|
||||
console.error("Errore login:", err);
|
||||
errorEl.textContent = "Errore di connessione al server";
|
||||
}
|
||||
});
|
||||
*/
|
||||
|
||||
// ===============================
|
||||
// LOGIN SUBMIT
|
||||
|
|
|
|||
|
|
@ -200,35 +200,70 @@ function startWebSocket() {
|
|||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = async (ev) => {
|
||||
let msg;
|
||||
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 (da /photos o auto_scan ADD)
|
||||
if (msg.type === "added") {
|
||||
console.log(`🟢 [WS] Foto aggiunta → id=${msg.photo_id || msg.id}`);
|
||||
|
||||
const id = msg.id;
|
||||
|
||||
try {
|
||||
msg = JSON.parse(ev.data);
|
||||
} catch (e) {
|
||||
console.error("❌ [WS] Errore parsing JSON:", e);
|
||||
return;
|
||||
}
|
||||
const arr = await getPhotoById(id);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// FOTO RIMOSSA (auto_scan DEL)
|
||||
if (msg.type === "removed") {
|
||||
const id = msg.id;
|
||||
console.log(`🔴 [WS] Foto rimossa → id=${id}`);
|
||||
|
||||
removePhotoLocal(id);
|
||||
|
||||
const el = document.getElementById("photo_" + id);
|
||||
if (el) {
|
||||
console.log(`🗑️ [WS] Rimuovo elemento DOM: ${el.id}`);
|
||||
el.remove();
|
||||
}
|
||||
|
||||
refreshGallery();
|
||||
}
|
||||
|
||||
// CARTELLA AGGIUNTA
|
||||
if (msg.type === "add_dir") {
|
||||
console.log(`📁 [WS] Cartella aggiunta → ${msg.folder}`);
|
||||
// qui puoi decidere se rifare una sync parziale o solo aggiornare la lista cartelle
|
||||
// per ora: incrementalSync "soft"
|
||||
incrementalSync();
|
||||
}
|
||||
|
||||
// CARTELLA RIMOSSA
|
||||
if (msg.type === "del_dir") {
|
||||
console.log(`📁 [WS] Cartella rimossa → ${msg.folder}`);
|
||||
// idem come sopra
|
||||
incrementalSync();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn("❌ [WS] Connessione chiusa → ritento tra 3s...");
|
||||
|
|
|
|||
|
|
@ -1,253 +0,0 @@
|
|||
// ===============================
|
||||
// getPhotoById — versione corretta per incrementalSync
|
||||
// ===============================
|
||||
async function getPhotoById(id) {
|
||||
const token = localStorage.getItem("token");
|
||||
const payload = parseJwt(token);
|
||||
const user = payload.name || "Common";
|
||||
|
||||
const url = `${BASE_URL}/photos/byIds?id=${encodeURIComponent(id)}&user=${encodeURIComponent(user)}`;
|
||||
|
||||
console.log("[DEBUG getPhotoById] URL:", url);
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(url, {
|
||||
headers: { "Authorization": "Bearer " + token }
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("[getPhotoById] ERRORE FETCH:", e);
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("[getPhotoById] ERRORE HTTP:", res.status, res.statusText);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const json = await res.json();
|
||||
console.log("[DEBUG getPhotoById] JSON:", json);
|
||||
return json;
|
||||
} catch (e) {
|
||||
console.error("[getPhotoById] ERRORE JSON:", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// sync.js — Sync incrementale con LOG DIAGNOSTICI COMPLETI
|
||||
// ===============================
|
||||
|
||||
async function incrementalSync() {
|
||||
console.log(">>> incrementalSync() START");
|
||||
|
||||
const payload = parseJwt(localStorage.getItem("token"));
|
||||
const user = payload.name || "Common";
|
||||
|
||||
let lastSync = getLastSync();
|
||||
const currentPhotos = getLocalPhotos();
|
||||
|
||||
console.log("[incrementalSync] lastSync =", lastSync, "user =", user);
|
||||
console.log("[incrementalSync] localPhotos attuali =", currentPhotos.length);
|
||||
|
||||
// ============================================================
|
||||
// 1) FULL LOAD OBBLIGATORIO
|
||||
// ============================================================
|
||||
if (!lastSync || currentPhotos.length === 0) {
|
||||
console.log("[incrementalSync] Primo avvio → fullLoad() (forzato)");
|
||||
|
||||
try {
|
||||
console.log("[DEBUG] Chiamo getAllPhotos()");
|
||||
const photos = await getAllPhotos();
|
||||
|
||||
console.log(`[incrementalSync] fullLoad() ha caricato ${photos.length} foto`);
|
||||
saveLocalState();
|
||||
refreshGallery();
|
||||
|
||||
const now = new Date().toISOString();
|
||||
setLastSync(now);
|
||||
|
||||
console.log("[incrementalSync] setLastSync (fullLoad) =", now);
|
||||
console.log(">>> incrementalSync() COMPLETATO (fullLoad)");
|
||||
return;
|
||||
|
||||
} catch (err) {
|
||||
console.error("[incrementalSync] ERRORE in fullLoad:", err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 2) SYNC INCREMENTALE
|
||||
// ============================================================
|
||||
let changes;
|
||||
try {
|
||||
console.log(`[DEBUG] Chiamo getChanges(lastSync=${lastSync}, user=${user})`);
|
||||
changes = await getChanges(lastSync, user);
|
||||
} catch (err) {
|
||||
console.error("[incrementalSync] ERRORE getChanges:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[incrementalSync] Cambiamenti ricevuti:", changes);
|
||||
|
||||
if (!changes || !changes.changes || changes.changes.length === 0) {
|
||||
console.log("[incrementalSync] Nessun cambiamento → lastSync NON aggiornato");
|
||||
return;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 3) APPLICA CAMBIAMENTI
|
||||
// ============================================================
|
||||
for (const ch of changes.changes) {
|
||||
console.log("--------------------------------------------------");
|
||||
console.log("[incrementalSync] Cambio:", ch);
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FOTO AGGIUNTA
|
||||
// ---------------------------------------------------------
|
||||
if (ch.change_type === "added") {
|
||||
console.log(`[DEBUG] Evento ADDED → richiedo getPhotoById(${ch.photo_id})`);
|
||||
|
||||
try {
|
||||
const arr = await getPhotoById(ch.photo_id);
|
||||
|
||||
console.log(`[DEBUG getPhotoById] id=${ch.photo_id} → risposta:`, arr);
|
||||
|
||||
if (arr.length) {
|
||||
console.log(`[DEBUG] Foto trovata → aggiungo a localPhotos id=${ch.photo_id}`);
|
||||
addPhotoLocal(arr[0]);
|
||||
} else {
|
||||
console.warn(`[WARN] Foto NON trovata nel server remoto per id=${ch.photo_id}`);
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error("[incrementalSync] ERRORE getPhotoById:", err);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// FOTO RIMOSSA
|
||||
// ---------------------------------------------------------
|
||||
if (ch.change_type === "removed") {
|
||||
console.log(`[DEBUG] Evento REMOVED → rimuovo foto id=${ch.photo_id}`);
|
||||
|
||||
removePhotoLocal(ch.photo_id);
|
||||
|
||||
const el = document.getElementById("photo_" + ch.photo_id);
|
||||
if (el) {
|
||||
console.log("[DEBUG] Rimuovo elemento DOM:", el.id);
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("[incrementalSync] localPhotos dopo sync:", getLocalPhotos().length);
|
||||
|
||||
// ============================================================
|
||||
// 4) REFRESH UI
|
||||
// ============================================================
|
||||
console.log("[DEBUG] refreshGallery()");
|
||||
refreshGallery();
|
||||
|
||||
// ============================================================
|
||||
// 5) AGGIORNA lastSync
|
||||
// ============================================================
|
||||
const lastTimestamp = changes.changes.at(-1).timestamp || new Date().toISOString();
|
||||
|
||||
console.log(`[DEBUG] Aggiorno lastSync → ${lastTimestamp}`);
|
||||
setLastSync(lastTimestamp);
|
||||
|
||||
console.log(">>> incrementalSync() COMPLETATO");
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// JWT PARSER
|
||||
// ===============================
|
||||
function parseJwt(token) {
|
||||
if (!token) return {};
|
||||
try {
|
||||
const base64 = token.split('.')[1];
|
||||
const json = atob(base64);
|
||||
return JSON.parse(json);
|
||||
} catch (e) {
|
||||
console.error("parseJwt error:", e);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// WEBSOCKET REAL-TIME
|
||||
// ===============================
|
||||
function startWebSocket() {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) {
|
||||
console.error("[WS] Nessun token JWT trovato");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = parseJwt(token);
|
||||
const user = payload.name || "Common";
|
||||
|
||||
console.log("[WS] Connessione a wss://prova-ws.patachina.it ...");
|
||||
|
||||
const ws = new WebSocket("wss://prova-ws.patachina.it");
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("[WS] Connesso, invio token JWT");
|
||||
|
||||
ws.send(JSON.stringify({
|
||||
type: "auth",
|
||||
token
|
||||
}));
|
||||
};
|
||||
|
||||
ws.onmessage = async (ev) => {
|
||||
let msg;
|
||||
try {
|
||||
msg = JSON.parse(ev.data);
|
||||
} catch (e) {
|
||||
console.error("[WS] Errore parsing JSON:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("[WS] Messaggio ricevuto:", msg);
|
||||
|
||||
// -------------------------------
|
||||
// FOTO AGGIUNTA
|
||||
// -------------------------------
|
||||
if (msg.type === "photo_added") {
|
||||
console.log(`[WS] Foto aggiunta → id=${msg.photo_id}`);
|
||||
|
||||
try {
|
||||
const arr = await getPhotoById(msg.photo_id);
|
||||
|
||||
if (arr.length) {
|
||||
console.log("[WS] Foto trovata, aggiungo a localPhotos");
|
||||
addPhotoLocal(arr[0]);
|
||||
refreshGallery();
|
||||
} else {
|
||||
console.warn("[WS] Foto non trovata nel server remoto");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[WS] Errore getPhotoById:", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.warn("[WS] Connessione chiusa, ritento tra 3s...");
|
||||
setTimeout(startWebSocket, 3000);
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.error("[WS] Errore WebSocket:", err);
|
||||
};
|
||||
}
|
||||
|
||||
// Avvio automatico del WebSocket
|
||||
startWebSocket();
|
||||
|
||||
window.incrementalSync = incrementalSync;
|
||||
window.parseJwt = parseJwt;
|
||||
|
Before Width: | Height: | Size: 4.1 MiB |
|
Before Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 3.1 MiB |
|
Before Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 4.2 MiB |
|
Before Width: | Height: | Size: 4.3 MiB |
|
Before Width: | Height: | Size: 4.5 MiB |
|
|
@ -1 +0,0 @@
|
|||
prova
|
||||
|
|
@ -1 +1 @@
|
|||
{"current":103,"total":103,"percent":100,"eta":"00:00:00","elapsed":"00:00:01"}
|
||||
{"current":22,"total":22,"percent":100,"eta":"00:00:00","elapsed":"00:00:11"}
|
||||
23067
public/scan.log
|
|
@ -3,8 +3,6 @@ const express = require('express');
|
|||
const router = express.Router();
|
||||
|
||||
const db = require('../db/knex');
|
||||
|
||||
// 🔥 IMPORTANTE: aggiungi questa riga
|
||||
const wss = require('../ws-server');
|
||||
|
||||
// -----------------------------------------------------
|
||||
|
|
@ -26,7 +24,6 @@ function normalizePhoto(p) {
|
|||
|
||||
_indexHash: p._indexHash || null,
|
||||
|
||||
// Rimuovo campi DB non necessari
|
||||
lat: undefined,
|
||||
lon: undefined,
|
||||
alt: undefined,
|
||||
|
|
@ -44,7 +41,6 @@ router.post('/', async (req, res) => {
|
|||
try {
|
||||
const p = req.body;
|
||||
|
||||
// 1) Inserisci la foto nel DB remoto
|
||||
console.log(`📸 [DB INSERT] Salvo foto id=${p.id} user=${p.user}`);
|
||||
await db('photos').insert({
|
||||
id: p.id,
|
||||
|
|
@ -71,7 +67,6 @@ router.post('/', async (req, res) => {
|
|||
fast_hash: p.fast_hash
|
||||
});
|
||||
|
||||
// 2) Registra l’evento ADDED
|
||||
console.log(`🟢 [DB CHANGE] Inserisco evento ADDED per id=${p.id}`);
|
||||
await db('photo_changes').insert({
|
||||
photo_id: p.id,
|
||||
|
|
@ -80,10 +75,9 @@ router.post('/', async (req, res) => {
|
|||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 🔥 3) INVIO EVENTO WEBSOCKET IN TEMPO REALE
|
||||
console.log(`📡 [WS] Invio evento real-time a user=${p.user}`);
|
||||
wss.broadcastToUser(p.user, {
|
||||
type: "photo_added",
|
||||
type: "added",
|
||||
photo_id: p.id
|
||||
});
|
||||
|
||||
|
|
@ -97,19 +91,29 @@ router.post('/', async (req, res) => {
|
|||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// GET /photos → restituisce TUTTE le foto dell’utente + Common
|
||||
// GET /photos → Admin vede tutto, altri vedono solo loro + Common
|
||||
// -----------------------------------------------------
|
||||
router.get('/', async (req, res) => {
|
||||
console.log("📤 [GET /photos] Richiesta ricevuta");
|
||||
const user = req.query.user || req.user?.name;
|
||||
console.log("📤 [GET /photos] Richiesta ricevuta user=", user);
|
||||
|
||||
try {
|
||||
const rows = await db('photos')
|
||||
.select('*')
|
||||
.orderBy('mtimeMs', 'desc');
|
||||
let rows;
|
||||
|
||||
if (user === "Admin") {
|
||||
console.log("👑 Admin → restituisco TUTTE le foto");
|
||||
rows = await db('photos')
|
||||
.select('*')
|
||||
.orderBy('mtimeMs', 'desc');
|
||||
} else {
|
||||
console.log(`👤 Utente normale → foto di ${user} + Common`);
|
||||
rows = await db('photos')
|
||||
.whereIn('user', [user, "Common"])
|
||||
.orderBy('mtimeMs', 'desc');
|
||||
}
|
||||
|
||||
console.log(`📤 [GET /photos] Restituisco ${rows.length} foto`);
|
||||
const normalized = rows.map(normalizePhoto);
|
||||
res.json(normalized);
|
||||
res.json(rows.map(normalizePhoto));
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Errore GET /photos:", err);
|
||||
|
|
@ -118,7 +122,7 @@ router.get('/', async (req, res) => {
|
|||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// GET /photos/changes
|
||||
// GET /photos/changes → Admin vede tutto
|
||||
// -----------------------------------------------------
|
||||
router.get('/changes', async (req, res) => {
|
||||
const since = req.query.since;
|
||||
|
|
@ -127,15 +131,24 @@ router.get('/changes', async (req, res) => {
|
|||
console.log(`📤 [GET /photos/changes] since=${since} users=${users}`);
|
||||
|
||||
if (!since || !users.length) {
|
||||
console.log("❌ Parametri mancanti");
|
||||
return res.status(400).json({ error: "Parametri richiesti: since, user" });
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await db('photo_changes')
|
||||
.where('timestamp', '>', since)
|
||||
.whereIn('user', users)
|
||||
.orderBy('timestamp', 'asc');
|
||||
let rows;
|
||||
|
||||
if (users.includes("Admin")) {
|
||||
console.log("👑 Admin → restituisco TUTTI i cambiamenti");
|
||||
rows = await db('photo_changes')
|
||||
.where('timestamp', '>', since)
|
||||
.orderBy('timestamp', 'asc');
|
||||
} else {
|
||||
console.log("👤 Utente normale → cambiamenti filtrati");
|
||||
rows = await db('photo_changes')
|
||||
.where('timestamp', '>', since)
|
||||
.whereIn('user', users)
|
||||
.orderBy('timestamp', 'asc');
|
||||
}
|
||||
|
||||
console.log(`📤 [GET /photos/changes] Restituisco ${rows.length} cambiamenti`);
|
||||
res.json({ changes: rows });
|
||||
|
|
@ -147,41 +160,35 @@ router.get('/changes', async (req, res) => {
|
|||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// GET /photos/byIds
|
||||
// GET /photos/byIds → Admin vede tutto
|
||||
// -----------------------------------------------------
|
||||
router.get('/byIds', async (req, res) => {
|
||||
const ids = req.query.id;
|
||||
const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user];
|
||||
|
||||
console.log("📤 [GET /photos/byIds] ids:", ids);
|
||||
console.log("📤 [GET /photos/byIds] users (raw):", req.query.user);
|
||||
console.log("📤 [GET /photos/byIds] users (array):", users);
|
||||
console.log("📤 [GET /photos/byIds] users:", users);
|
||||
|
||||
if (!ids) {
|
||||
console.log("⚠️ Nessun id richiesto → []");
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
if (!users || users.length === 0 || users[0] === undefined) {
|
||||
console.log("❌ [GET /photos/byIds] ERRORE: 'user' NON passato dal frontend!");
|
||||
}
|
||||
if (!ids) return res.json([]);
|
||||
|
||||
const list = Array.isArray(ids) ? ids : [ids];
|
||||
console.log("📤 [GET /photos/byIds] lista finale id:", list);
|
||||
|
||||
try {
|
||||
console.log("📤 [GET /photos/byIds] Eseguo query con:");
|
||||
console.log(" → id IN", list);
|
||||
console.log(" → user IN", users);
|
||||
let rows;
|
||||
|
||||
const rows = await db('photos')
|
||||
.whereIn('id', list)
|
||||
.whereIn('user', users);
|
||||
if (users.includes("Admin")) {
|
||||
console.log("👑 Admin → byIds senza filtro user");
|
||||
rows = await db('photos')
|
||||
.whereIn('id', list);
|
||||
} else {
|
||||
console.log("👤 Utente normale → byIds filtrato");
|
||||
rows = await db('photos')
|
||||
.whereIn('id', list)
|
||||
.whereIn('user', users);
|
||||
}
|
||||
|
||||
console.log(`📤 [GET /photos/byIds] Query completata → trovate ${rows.length} foto`);
|
||||
|
||||
const normalized = rows.map(normalizePhoto);
|
||||
res.json(normalized);
|
||||
console.log(`📤 [GET /photos/byIds] trovate ${rows.length} foto`);
|
||||
res.json(rows.map(normalizePhoto));
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Errore /photos/byIds:", err);
|
||||
|
|
@ -189,4 +196,4 @@ router.get('/byIds', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
module.exports = router;
|
||||
|
|
|
|||
193
routes/photos.js.old
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
// routes/photos.js
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
|
||||
const db = require('../db/knex');
|
||||
|
||||
// 🔥 IMPORTANTE: aggiungi questa riga
|
||||
const wss = require('../ws-server');
|
||||
|
||||
// -----------------------------------------------------
|
||||
// Normalizzazione record DB → formato identico al vecchio index.json
|
||||
// -----------------------------------------------------
|
||||
function normalizePhoto(p) {
|
||||
return {
|
||||
...p,
|
||||
|
||||
gps: (p.lat != null && p.lon != null)
|
||||
? {
|
||||
lat: p.lat,
|
||||
lng: p.lon,
|
||||
alt: p.alt ?? null
|
||||
}
|
||||
: null,
|
||||
|
||||
location: p.location ? JSON.parse(p.location) : null,
|
||||
|
||||
_indexHash: p._indexHash || null,
|
||||
|
||||
// Rimuovo campi DB non necessari
|
||||
lat: undefined,
|
||||
lon: undefined,
|
||||
alt: undefined,
|
||||
location_json: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------
|
||||
// POST /photos → salva una foto inviata dallo scanner locale
|
||||
// -----------------------------------------------------
|
||||
router.post('/', async (req, res) => {
|
||||
console.log("📥 [POST /photos] Richiesta ricevuta");
|
||||
console.log("📥 Body:", req.body);
|
||||
|
||||
try {
|
||||
const p = req.body;
|
||||
|
||||
// 1) Inserisci la foto nel DB remoto
|
||||
console.log(`📸 [DB INSERT] Salvo foto id=${p.id} user=${p.user}`);
|
||||
await db('photos').insert({
|
||||
id: p.id,
|
||||
user: p.user,
|
||||
cartella: p.cartella,
|
||||
name: p.name,
|
||||
path: p.path,
|
||||
thub1: p.thub1,
|
||||
thub2: p.thub2,
|
||||
mime_type: p.mime_type,
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
rotation: p.rotation,
|
||||
size_bytes: p.size_bytes,
|
||||
mtimeMs: p.mtimeMs,
|
||||
duration_ms: p.duration_ms,
|
||||
taken_at: p.taken_at,
|
||||
data: p.data,
|
||||
lat: p.gps?.lat ?? null,
|
||||
lon: p.gps?.lng ?? null,
|
||||
alt: p.gps?.alt ?? null,
|
||||
location: p.location ? JSON.stringify(p.location) : null,
|
||||
_indexHash: p._indexHash,
|
||||
fast_hash: p.fast_hash
|
||||
});
|
||||
|
||||
// 2) Registra 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: "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) => {
|
||||
const user = req.query.user || req.user?.name;
|
||||
console.log("📤 [GET /photos] Richiesta ricevuta");
|
||||
|
||||
try {
|
||||
const rows = await db('photos')
|
||||
.whereIn('user', [user, "Common"])
|
||||
.orderBy('mtimeMs', 'desc');
|
||||
|
||||
console.log(`📤 [GET /photos] user=${user} Restituisco ${rows.length} foto`);
|
||||
const normalized = rows.map(normalizePhoto);
|
||||
res.json(normalized);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Errore GET /photos:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// GET /photos/changes
|
||||
// -----------------------------------------------------
|
||||
router.get('/changes', async (req, res) => {
|
||||
const since = req.query.since;
|
||||
const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user];
|
||||
|
||||
console.log(`📤 [GET /photos/changes] since=${since} users=${users}`);
|
||||
|
||||
if (!since || !users.length) {
|
||||
console.log("❌ Parametri mancanti");
|
||||
return res.status(400).json({ error: "Parametri richiesti: since, user" });
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await db('photo_changes')
|
||||
.where('timestamp', '>', since)
|
||||
.whereIn('user', users)
|
||||
.orderBy('timestamp', 'asc');
|
||||
|
||||
console.log(`📤 [GET /photos/changes] Restituisco ${rows.length} cambiamenti`);
|
||||
res.json({ changes: rows });
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Errore /photos/changes:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------------------------------------------
|
||||
// GET /photos/byIds
|
||||
// -----------------------------------------------------
|
||||
router.get('/byIds', async (req, res) => {
|
||||
const ids = req.query.id;
|
||||
const users = Array.isArray(req.query.user) ? req.query.user : [req.query.user];
|
||||
|
||||
console.log("📤 [GET /photos/byIds] ids:", ids);
|
||||
console.log("📤 [GET /photos/byIds] users (raw):", req.query.user);
|
||||
console.log("📤 [GET /photos/byIds] users (array):", users);
|
||||
|
||||
if (!ids) {
|
||||
console.log("⚠️ Nessun id richiesto → []");
|
||||
return res.json([]);
|
||||
}
|
||||
|
||||
if (!users || users.length === 0 || users[0] === undefined) {
|
||||
console.log("❌ [GET /photos/byIds] ERRORE: 'user' NON passato dal frontend!");
|
||||
}
|
||||
|
||||
const list = Array.isArray(ids) ? ids : [ids];
|
||||
console.log("📤 [GET /photos/byIds] lista finale id:", list);
|
||||
|
||||
try {
|
||||
console.log("📤 [GET /photos/byIds] Eseguo query con:");
|
||||
console.log(" → id IN", list);
|
||||
console.log(" → user IN", users);
|
||||
|
||||
const rows = await db('photos')
|
||||
.whereIn('id', list)
|
||||
.whereIn('user', users);
|
||||
|
||||
console.log(`📤 [GET /photos/byIds] Query completata → trovate ${rows.length} foto`);
|
||||
|
||||
const normalized = rows.map(normalizePhoto);
|
||||
res.json(normalized);
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ Errore /photos/byIds:", err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
554
scanPhoto_how_works.md
Normal file
|
|
@ -0,0 +1,554 @@
|
|||
|
||||
📌 1. Avvio dello scan
|
||||
- Parte con un log: "Inizio scan globale per user=X".
|
||||
- Calcola il percorso della cartella photos/<user>/original.
|
||||
- Conta velocemente quanti file supportati esistono (con countFilesFast).
|
||||
- Se non ci sono file → termina.
|
||||
|
||||
|
||||
📊 2. Gestione dello stato di avanzamento
|
||||
Durante lo scan:
|
||||
- Tiene traccia di:
|
||||
- CURRENT = quanti file ha già processato
|
||||
- TOTAL_FILES = quanti file totali ci sono
|
||||
- Aggiorna un file JSON pubblico:
|
||||
public/photos/scan_status.json
|
||||
|
||||
Questo file contiene:
|
||||
- current
|
||||
- total
|
||||
- percent
|
||||
- elapsed
|
||||
- eta
|
||||
|
||||
Serve per mostrare una barra di avanzamento in tempo reale.
|
||||
|
||||
|
||||
📁 3. Scansione delle cartelle dell’utente
|
||||
Per ogni sottocartella dentro original/:
|
||||
- Chiama scanSingleFolder(user, cartella, absCartella).
|
||||
|
||||
Dentro questa funzione succede il grosso del lavoro.
|
||||
|
||||
|
||||
🔍 4. Per ogni file trovato:
|
||||
scanCartella() restituisce oggetti con:
|
||||
- id (deterministico)
|
||||
- name
|
||||
- relPath
|
||||
- absPath
|
||||
- ext
|
||||
- stat (size, mtime, ecc.)
|
||||
- path normalizzato
|
||||
|
||||
user=Fabio cartella=2017Irlanda19-29ago relPath=VID_20260317_115145.mp4 id=51a7e1861de8da5147bbbc35d70a0128fcd2a0451c7121a8bf2412cb2903d469
|
||||
|
||||
Per ogni file:
|
||||
|
||||
🟦 4.1 Recupera eventuale record esistente dal DB
|
||||
`js
|
||||
prev = await db("photos").where({ id }).first();
|
||||
`
|
||||
|
||||
🟩 4.2 Se il path è cambiato → considerato nuovo
|
||||
`js
|
||||
if (prev && prev.path !== f.path) prev = null;
|
||||
`
|
||||
|
||||
🟢 4.3 Se è un nuovo file
|
||||
- Log dettagliato
|
||||
- Chiama processFile() per generare metadati, thumbnails, EXIF, ecc.
|
||||
- Inserisce nel DB
|
||||
- Registra un evento in photo_changes
|
||||
- Aggiunge a newFiles
|
||||
|
||||
🔵 4.4 Fast-skip (file invariato)
|
||||
Due livelli:
|
||||
|
||||
1. FAST-SIZE-SKIP
|
||||
Se la size è identica → aggiorna solo path/mtime/hash.
|
||||
|
||||
2. FAST-HASH-SKIP
|
||||
Se l’hash veloce (sha256(size-mtime)) è identico → aggiorna solo path/mtime.
|
||||
|
||||
🟠 4.5 File modificato
|
||||
Se non passa i fast-skip:
|
||||
- Rielabora il file con processFile()
|
||||
- Aggiorna il DB con .insert().onConflict().merge()
|
||||
- Registra evento updated
|
||||
- Aggiunge a newFiles
|
||||
|
||||
---
|
||||
|
||||
🗑️ 5. Gestione degli orfani
|
||||
Alla fine della cartella:
|
||||
- Tutti gli ID presenti nel DB ma non trovati sul disco vengono:
|
||||
- rimossi dal DB
|
||||
- rimossi i thumbnails
|
||||
- loggati come cancellati
|
||||
|
||||
---
|
||||
|
||||
🌐 6. Invio al server remoto (opzionale)
|
||||
Se SENDPHOTOS=true e BASEURL è configurato:
|
||||
- Ogni nuovo file viene inviato via postWithAuth() al server remoto.
|
||||
|
||||
Se non configurato:
|
||||
- Logga [NO SEND].
|
||||
|
||||
---
|
||||
|
||||
🟣 7. Fine scan
|
||||
Logga il tempo totale e restituisce l’array newFiles.
|
||||
|
||||
---
|
||||
|
||||
🧩 In sintesi
|
||||
scanPhoto.js è un motore di sincronizzazione locale → database → server remoto.
|
||||
|
||||
Fa tutto questo:
|
||||
|
||||
| Funzione | Descrizione |
|
||||
|---------|-------------|
|
||||
| Scansione ricorsiva | Trova tutti i file supportati nelle cartelle dell’utente |
|
||||
| Confronto con DB | Determina se un file è nuovo, modificato o invariato |
|
||||
| Fast-skip | Evita elaborazioni inutili usando size e hash veloce |
|
||||
| Elaborazione file | Chiama processFile() per EXIF, thumbnails, metadati |
|
||||
| Aggiornamento DB | Inserisce/aggiorna record in photos |
|
||||
| Rilevamento orfani | Cancella dal DB file non più presenti sul disco |
|
||||
| Log avanzamento | Scrive un file JSON con percentuale, ETA, ecc. |
|
||||
| Invio remoto | Invia i nuovi file a un server remoto (se abilitato) |
|
||||
|
||||
_____
|
||||
|
||||
functio countFilesFast(rootDir)
|
||||
conta i file nelle cartelle photos/<user>/original
|
||||
|
||||
function formatTime(ms)
|
||||
trasforma ms in h:m:s
|
||||
|
||||
---
|
||||
|
||||
async function scanPhoto(dir, userName, db)
|
||||
|
||||
const photosRoot
|
||||
è il path assoluto <app>/public/photos
|
||||
|
||||
const userDir
|
||||
è il path assoluto <app>/public/photos/<user>/original
|
||||
|
||||
function updateStatusFile()
|
||||
fa l'aggiornamento dello stato di scansione dei file
|
||||
|
||||
function scanSingleFolder(user, cartella, absCartella)
|
||||
user: Fabio
|
||||
cartella: 2017Irlanda19-29ago
|
||||
absCartella: /home/nvme/dockerdata/prove/s15j/public/photos/Fabio/original/2017Irlanda19-29ago
|
||||
_____
|
||||
|
||||
|
||||
🧩 Cosa fa esattamente questa riga
|
||||
|
||||
```js
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
```
|
||||
---
|
||||
Quella riga fa una cosa molto semplice… ma potentissima.
|
||||
E soprattutto è il cuore della pulizia degli orfani.
|
||||
|
||||
|
||||
📌 0. definita da
|
||||
```
|
||||
const createCleanupFunctions = require('./orphanCleanup');
|
||||
```
|
||||
|
||||
🔍 1. Chiama createCleanupFunctions(db)
|
||||
createCleanupFunctions è una funzione che riceve il database (db) e restituisce un oggetto contenente due funzioni:
|
||||
|
||||
- deleteThumbsById(id)
|
||||
- deleteFromDB(id, user)
|
||||
|
||||
In pratica, è un factory: crea funzioni già configurate con il DB.
|
||||
|
||||
---
|
||||
|
||||
📌 2. Cosa fanno le due funzioni restituite
|
||||
|
||||
---
|
||||
🗑️ deleteThumbsById(id)
|
||||
Cancella i thumbnails associati a una foto:
|
||||
|
||||
- thub1
|
||||
- thub2
|
||||
- eventuali altre miniature generate
|
||||
|
||||
Serve quando un file non esiste più sul disco → quindi i suoi thumbnails diventano spazzatura.
|
||||
|
||||
---
|
||||
|
||||
🗄️ deleteFromDB(id, user)
|
||||
Cancella dal database:
|
||||
|
||||
- la riga nella tabella photos
|
||||
- eventuali metadati collegati
|
||||
- eventuali record in photo_changes (dipende da come l’hai implementato)
|
||||
|
||||
È la parte che rimuove l’entry dal DB quando il file è sparito dal filesystem.
|
||||
|
||||
---
|
||||
|
||||
🔴 3. Perché serve?
|
||||
Durante lo scan, alla fine di ogni cartella, hai:
|
||||
|
||||
`js
|
||||
for (const orphanId of idsSet) {
|
||||
log( 🔴 Cancellato ${orphanId});
|
||||
await deleteThumbsById(orphanId);
|
||||
await deleteFromDB(orphanId, user);
|
||||
}
|
||||
`
|
||||
|
||||
idsSet contiene gli ID presenti nel DB ma non trovati sul disco.
|
||||
|
||||
Quindi:
|
||||
|
||||
- deleteThumbsById() → pulisce i file fisici
|
||||
- deleteFromDB() → pulisce il database
|
||||
|
||||
È la logica che mantiene il sistema coerente.
|
||||
|
||||
---
|
||||
|
||||
🧠 In sintesi
|
||||
Quella riga:
|
||||
|
||||
`js
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
`Eccoti una spiegazione cristallina e completa di cosa fa:
|
||||
|
||||
`js
|
||||
async function scanSingleFolder(user, cartella, absCartella)
|
||||
`
|
||||
|
||||
È una funzione fondamentale: gestisce tutta la sincronizzazione tra filesystem e database per una singola cartella dell’utente.
|
||||
|
||||
---
|
||||
|
||||
📁 Obiettivo della funzione
|
||||
Scansionare una cartella (es. DCIM, Vacanze2023, WhatsApp Images) e:
|
||||
|
||||
1. Trovare i file presenti sul disco
|
||||
2. Confrontarli con il database
|
||||
3. Capire cosa è nuovo, cosa è modificato, cosa è invariato
|
||||
4. Aggiornare il DB
|
||||
5. Elaborare i nuovi file (EXIF, thumbnails, metadati)
|
||||
6. Rimuovere dal DB i file orfani (presenti nel DB ma non più sul disco)
|
||||
|
||||
È il cuore della logica di sincronizzazione.
|
||||
|
||||
---
|
||||
|
||||
🧠 Funzionamento passo per passo
|
||||
|
||||
1️⃣ Log di inizio
|
||||
`js
|
||||
log(📁 Inizio cartella: ${cartella});
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
2️⃣ Recupera dal DB tutti gli ID già conosciuti
|
||||
`js
|
||||
const rows = await db('photos')
|
||||
.where({ user, cartella })
|
||||
.select('id');
|
||||
|
||||
const idsSet = new Set(rows.map(r => r.id));
|
||||
`
|
||||
|
||||
idsSet serve per capire quali file non verranno trovati sul disco → saranno orfani.
|
||||
|
||||
---
|
||||
|
||||
3️⃣ Itera su tutti i file trovati sul disco
|
||||
`js
|
||||
for await (const f of scanCartella(...)) {
|
||||
`
|
||||
|
||||
scanCartella() restituisce per ogni file:
|
||||
|
||||
- id deterministico
|
||||
- name
|
||||
- path normalizzato
|
||||
- relPath, absPath
|
||||
- ext
|
||||
- stat (size, mtime, ecc.)
|
||||
|
||||
---
|
||||
|
||||
4️⃣ Aggiorna lo stato di avanzamento
|
||||
Incrementa CURRENT e aggiorna il file JSON con percentuale, ETA, ecc.
|
||||
|
||||
---
|
||||
|
||||
5️⃣ Recupera eventuale record esistente dal DB
|
||||
`js
|
||||
let prev = await db("photos")
|
||||
.select(...)
|
||||
.where({ id })
|
||||
.first();
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
6️⃣ Calcola il fast-hash
|
||||
`js
|
||||
const fastHash = sha256(${st.size}-${st.mtimeMs});
|
||||
`
|
||||
|
||||
Serve per capire velocemente se il file è cambiato.
|
||||
|
||||
---
|
||||
|
||||
🔍 7️⃣ Logica decisionale per ogni file
|
||||
|
||||
🟢 Caso A — File nuovo
|
||||
Se prev è null:
|
||||
|
||||
- Log
|
||||
- Chiama processFile() per estrarre metadati, EXIF, thumbnails
|
||||
- Inserisce nel DB
|
||||
- Registra evento added
|
||||
- Aggiunge a newFiles
|
||||
- Rimuove l’ID da idsSet
|
||||
|
||||
---
|
||||
|
||||
🔵 Caso B — File invariato (fast-skip)
|
||||
|
||||
1. FAST-SIZE-SKIP
|
||||
Se la size è identica → file invariato.
|
||||
|
||||
2. FAST-HASH-SKIP
|
||||
Se l’hash veloce è identico → invariato.
|
||||
|
||||
In entrambi i casi:
|
||||
|
||||
- Aggiorna solo path/mtime/hash nel DB
|
||||
- Rimuove l’ID da idsSet
|
||||
|
||||
---
|
||||
|
||||
🟠 Caso C — File modificato
|
||||
Se non passa i fast-skip:
|
||||
|
||||
- Log
|
||||
- Chiama processFile()
|
||||
- Aggiorna il DB con .insert().onConflict().merge()
|
||||
- Registra evento updated
|
||||
- Aggiunge a newFiles
|
||||
- Rimuove l’ID da idsSet
|
||||
|
||||
---
|
||||
|
||||
🔴 8️⃣ Gestione degli orfani
|
||||
Alla fine della cartella:
|
||||
|
||||
`js
|
||||
for (const orphanId of idsSet) {
|
||||
await deleteThumbsById(orphanId);
|
||||
await deleteFromDB(orphanId, user);
|
||||
}
|
||||
`
|
||||
|
||||
Questi sono file che:
|
||||
|
||||
- esistono nel DB
|
||||
- non sono stati trovati sul disco
|
||||
|
||||
Quindi vengono eliminati.
|
||||
|
||||
---
|
||||
|
||||
🧩 In sintesi
|
||||
scanSingleFolder():
|
||||
|
||||
| Step | Cosa fa |
|
||||
|------|---------|
|
||||
| 1 | Carica dal DB gli ID della cartella |
|
||||
| 2 | Scansiona i file reali sul disco |
|
||||
| 3 | Per ogni file decide: nuovo, modificato, invariato |
|
||||
| 4 | Elabora i nuovi/modificati con processFile() |
|
||||
| 5 | Aggiorna o inserisce nel DB |
|
||||
| 6 | Registra gli eventi in photo_changes |
|
||||
| 7 | Rimuove gli orfani (DB → disco) |
|
||||
| 8 | Restituisce i nuovi file |
|
||||
|
||||
È la funzione che mantiene coerenza totale tra filesystem e database.
|
||||
|
||||
---
|
||||
|
||||
Se vuoi, posso anche:
|
||||
|
||||
- disegnarti un diagramma di flusso
|
||||
- riscrivere la funzione in modo più modulare
|
||||
- aggiungere logging più leggibile
|
||||
- ottimizzare la parte di fast-skip
|
||||
- aggiungere batching per il DB
|
||||
|
||||
Dimmi tu come vuoi procedere.
|
||||
|
||||
fa questo:
|
||||
|
||||
- chiama una funzione factory
|
||||
- che crea due funzioni già configurate con il DB
|
||||
- che servono per cancellare foto orfane (thumbnails + DB)
|
||||
|
||||
È un modo elegante per incapsulare la logica di cleanup.
|
||||
|
||||
___
|
||||
|
||||
Eccoti la spiegazione chiara, completa e tecnica di cosa fa scanCartella.js e cosa restituisce.
|
||||
|
||||
---
|
||||
|
||||
📌 Cos’è scanCartella?
|
||||
È una funzione asincrona generator (async function*) che:
|
||||
|
||||
- scansiona ricorsivamente una cartella sul filesystem
|
||||
- filtra solo i file con estensioni supportate
|
||||
- calcola un ID deterministico per ogni file
|
||||
- raccoglie informazioni minime (stat, path, nome, estensione…)
|
||||
- NON elabora il file (niente EXIF, niente thumbnails)
|
||||
- NON scrive sul database
|
||||
|
||||
👉 Il suo unico scopo è enumerare i file reali presenti sul disco e restituire un oggetto per ciascuno.
|
||||
|
||||
È volutamente “leggera”: tutta la logica pesante (processFile, DB, confronto) è in scanPhoto.
|
||||
|
||||
---
|
||||
|
||||
🧠 Cosa fa, passo per passo
|
||||
|
||||
1️⃣ Legge la cartella ricorsivamente
|
||||
La funzione interna walk():
|
||||
|
||||
- usa fsp.readdir() con { withFileTypes: true }
|
||||
- entra nelle sottocartelle
|
||||
- ignora file non supportati
|
||||
|
||||
---
|
||||
|
||||
2️⃣ Filtra per estensioni supportate
|
||||
`js
|
||||
if (!SUPPORTED_EXTS.has(ext)) continue;
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
3️⃣ Costruisce il percorso relativo
|
||||
`js
|
||||
const fileRelPath = relPath ? ${relPath}/${e.name} : e.name;
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
4️⃣ Genera un ID deterministico
|
||||
`js
|
||||
const id = sha256(${userName}/${cartella}/${fileRelPath});
|
||||
`
|
||||
|
||||
Questo ID è:
|
||||
|
||||
- stabile
|
||||
- identico tra scansioni
|
||||
- basato su user/cartella/percorso relativo
|
||||
|
||||
È fondamentale per confrontare file reali ↔ DB.
|
||||
|
||||
---
|
||||
|
||||
5️⃣ Legge lo stat del file
|
||||
`js
|
||||
st = await fsp.stat(absPath);
|
||||
`
|
||||
|
||||
Serve per:
|
||||
|
||||
- size
|
||||
- mtimeMs
|
||||
- fast-hash
|
||||
- confronto rapido in scanPhoto
|
||||
|
||||
---
|
||||
|
||||
6️⃣ Logga il file trovato
|
||||
`js
|
||||
log(📂 scanCartella → user=${userName} cartella=${cartella} relPath=${fileRelPath} id=${id});
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
7️⃣ Restituisce un oggetto per ogni file
|
||||
Usa yield per restituire uno stream di risultati, non un array.
|
||||
|
||||
---
|
||||
|
||||
📤 Cosa restituisce esattamente?
|
||||
|
||||
Per ogni file valido, restituisce un oggetto:
|
||||
|
||||
`js
|
||||
{
|
||||
id, // ID deterministico
|
||||
user: userName, // nome utente
|
||||
cartella, // nome cartella (es. DCIM)
|
||||
name: e.name, // nome file
|
||||
relPath: fileRelPath, // percorso relativo dentro la cartella
|
||||
absPath, // percorso assoluto sul disco
|
||||
ext, // estensione (es. .jpg)
|
||||
stat: st, // fs.stat() → size, mtimeMs, ecc.
|
||||
path: /photos/${userName}/original/${cartella}/${fileRelPath}
|
||||
}
|
||||
`
|
||||
|
||||
---
|
||||
|
||||
🧩 In sintesi
|
||||
|
||||
✔️ Cosa fa
|
||||
- Scansiona ricorsivamente una cartella
|
||||
- Filtra solo i file supportati
|
||||
- Genera ID deterministici
|
||||
- Legge stat del file
|
||||
- Costruisce percorsi relativi/assoluti
|
||||
- Restituisce un oggetto per ogni file
|
||||
|
||||
❌ Cosa NON fa
|
||||
- Non elabora il file (niente EXIF, niente thumbnails)
|
||||
- Non scrive nel DB
|
||||
- Non decide se il file è nuovo/modificato
|
||||
- Non cancella orfani
|
||||
|
||||
---
|
||||
|
||||
🎯 A cosa serve?
|
||||
È il motore di enumerazione dei file.
|
||||
scanPhoto() usa i dati prodotti da scanCartella() per:
|
||||
|
||||
- confrontare con il DB
|
||||
- decidere se un file è nuovo/modificato/invariato
|
||||
- elaborarlo con processFile()
|
||||
- aggiornare il DB
|
||||
- eliminare orfani
|
||||
|
||||
---
|
||||
|
||||
Se vuoi, posso anche:
|
||||
|
||||
- disegnarti un diagramma di flusso
|
||||
- riscrivere scanCartella in modo più modulare
|
||||
- aggiungere caching o parallelizzazione
|
||||
- aggiungere controlli per file corrotti
|
||||
|
||||
Dimmi tu come vuoi procedere.
|
||||
471
scan_Funzionamento.md
Normal file
|
|
@ -0,0 +1,471 @@
|
|||
Perfetto Fabio, ora che il server è sistemato posso spiegarti **esattamente come funziona lo scan adesso**, in modo chiaro, ordinato e soprattutto *vero* rispetto al codice che hai.
|
||||
|
||||
Ti faccio una spiegazione **operativa**, come se stessi guardando il flusso in tempo reale.
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **1. L’utente chiama `/scan`**
|
||||
Quando fai:
|
||||
|
||||
```
|
||||
GET /scan
|
||||
```
|
||||
|
||||
succede questo:
|
||||
|
||||
### ✔ Il middleware JWT autentica l’utente
|
||||
### ✔ Il middleware GET *non aggiunge più “Common”*
|
||||
→ quindi lo scan viene fatto **solo per l’utente reale**
|
||||
→ niente doppie scansioni
|
||||
→ niente doppio removed
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **2. Il server chiama `scanPhotosUser(userName, db)`**
|
||||
|
||||
Esempio:
|
||||
|
||||
```
|
||||
scanPhotosUser("Fabio", db)
|
||||
```
|
||||
|
||||
Questa funzione fa due passaggi:
|
||||
|
||||
---
|
||||
|
||||
# 🟣 **PASSAGGIO 1 — Conta i file reali (ricorsivo)**
|
||||
`countFilesUser()`:
|
||||
|
||||
- entra in `/photos/Fabio/original`
|
||||
- scende in tutte le sottocartelle
|
||||
- conta solo i file con estensioni supportate
|
||||
- NON tocca il DB
|
||||
- NON scansiona due volte
|
||||
- NON elimina orfani
|
||||
|
||||
Questo serve solo per:
|
||||
|
||||
- mostrare il progresso
|
||||
- calcolare la percentuale
|
||||
- stimare il tempo
|
||||
|
||||
E ora funziona correttamente.
|
||||
|
||||
---
|
||||
|
||||
# 🟣 **PASSAGGIO 2 — Scansiona ogni cartella UNA sola volta**
|
||||
|
||||
Per ogni cartella dentro:
|
||||
|
||||
```
|
||||
/photos/Fabio/original/
|
||||
```
|
||||
|
||||
viene chiamato:
|
||||
|
||||
```
|
||||
scanPhoto(cartella, userName, db, ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **3. `scanPhoto()` chiama `scanPhotoCartella()` UNA sola volta**
|
||||
|
||||
E qui avviene la vera magia.
|
||||
|
||||
---
|
||||
|
||||
# 🟣 **Dentro `scanPhotoCartella()` succede questo:**
|
||||
|
||||
### 1️⃣ Recupera dal DB tutti gli ID della cartella
|
||||
→ costruisce `idsSet`
|
||||
|
||||
### 2️⃣ Scansiona i file reali con `scanCartella()`
|
||||
→ per ogni file trovato:
|
||||
- aggiorna il progresso
|
||||
- chiama `scanPhotoSingle()` per processarlo
|
||||
- elimina l’ID da `idsSet` (non è orfano)
|
||||
|
||||
### 3️⃣ Alla fine, gli ID rimasti in `idsSet` sono **orfani veri**
|
||||
|
||||
### 4️⃣ Per ogni orfano:
|
||||
- elimina thumbs
|
||||
- elimina dal DB
|
||||
- registra **1 solo removed**
|
||||
|
||||
---
|
||||
|
||||
# 🔥 **Perché ora NON hai più 2 removed?**
|
||||
|
||||
Perché:
|
||||
|
||||
- `/scan` viene chiamato **una sola volta**
|
||||
- `scanPhotosUser` viene eseguito **una sola volta**
|
||||
- `scanPhoto` viene eseguito **una sola volta**
|
||||
- `scanPhotoCartella` viene eseguito **una sola volta**
|
||||
- gli orfani vengono eliminati **una sola volta**
|
||||
|
||||
Prima invece:
|
||||
|
||||
- il middleware GET aggiungeva **Common**
|
||||
- quindi `/scan` scansionava **Fabio + Common**
|
||||
- Common vedeva gli ID di Fabio come orfani
|
||||
→ removed
|
||||
- Fabio vedeva gli ID come orfani
|
||||
→ removed
|
||||
- totale: **2 removed**
|
||||
|
||||
Ora questo non succede più.
|
||||
|
||||
---
|
||||
|
||||
# 🟢 **RISULTATO FINALE DEL NUOVO SISTEMA**
|
||||
|
||||
### ✔ 1 added quando aggiungi una foto
|
||||
### ✔ 1 removed quando la togli
|
||||
### ✔ nessun doppio evento
|
||||
### ✔ nessuna doppia scansione
|
||||
### ✔ conteggio file corretto
|
||||
### ✔ comportamento stabile e identico al vecchio sistema
|
||||
|
||||
---
|
||||
|
||||
Perfetto Fabio — ti preparo **un diagramma chiaro, leggibile e fedele al comportamento reale del tuo sistema**, così puoi vedere tutto il flusso in un colpo d’occhio.
|
||||
|
||||
Lo faccio in stile **flowchart testuale**, così puoi anche stamparlo o incollarlo nei tuoi appunti.
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **DIAGRAMMA COMPLETO DELLO SCAN (versione attuale, corretta)**
|
||||
|
||||
```
|
||||
┌──────────────────────────────┐
|
||||
│ CLIENT /scan │
|
||||
└───────────────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 1. Middleware JWT │
|
||||
│ ✔ Autentica l’utente │
|
||||
│ ✔ Imposta req.user │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 2. Middleware GET (PATCH) │
|
||||
│ ✔ NON aggiunge più "Common" │
|
||||
│ ✔ Per /scan → req.query.user = [utente] │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 3. Route /scan │
|
||||
│ chiama: scanPhotosUser(req.user.name, db) │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 4. scanPhotosUser(user) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 4.1 PRIMA PASSATA (ricorsiva) │ │
|
||||
│ │ countFilesUser() │ │
|
||||
│ │ ✔ entra in tutte le sottocartelle│ │
|
||||
│ │ ✔ conta SOLO i file supportati │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 4.2 SECONDA PASSATA │ │
|
||||
│ │ per ogni cartella in /original │ │
|
||||
│ │ → scanPhoto(cartella) │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 5. scanPhoto(cartella) │
|
||||
│ chiama: scanPhotoCartella() │
|
||||
│ (UNA sola volta per cartella) │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 6. scanPhotoCartella() │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 6.1 Recupera ID dal DB │ │
|
||||
│ │ → idsSet = { id1, id2, ... } │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 6.2 Scansione reale dei file │ │
|
||||
│ │ for await (f of scanCartella) │ │
|
||||
│ │ → scanPhotoSingle(f) │ │
|
||||
│ │ → idsSet.delete(f.id) │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────────┐ │
|
||||
│ │ 6.3 ORFANI (alla fine) │ │
|
||||
│ │ per ogni id rimasto in idsSet: │ │
|
||||
│ │ → deleteThumbsById │ │
|
||||
│ │ → deleteFromDB │ │
|
||||
│ │ → recordChange("removed") │ │
|
||||
│ └──────────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 7. scanPhoto ritorna newFiles │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 8. scanPhotosUser aggrega tutti i newFiles │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 9. /scan risponde al client │
|
||||
└──────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 🟢 **Cosa garantisce questo flusso**
|
||||
|
||||
### ✔ `/scan` viene eseguito UNA sola volta
|
||||
### ✔ `scanPhotosUser` viene eseguito UNA sola volta
|
||||
### ✔ `scanPhoto` viene eseguito UNA sola volta per cartella
|
||||
### ✔ `scanPhotoCartella` viene eseguito UNA sola volta
|
||||
### ✔ gli orfani vengono eliminati UNA sola volta
|
||||
### ✔ 1 added, 1 removed (mai più 2 removed)
|
||||
### ✔ conteggio file corretto
|
||||
### ✔ nessuna interferenza con “Common”
|
||||
|
||||
---
|
||||
|
||||
# 🔥 Vuoi anche la versione “diagramma a blocchi” o “diagramma a frecce” più compatto?
|
||||
# Se vuoi, posso anche farti un diagramma visivo del flusso completo, così lo vedi in un colpo d’occhio.
|
||||
|
||||
┌──────────────┐
|
||||
│ /scan │
|
||||
└───────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ 1. Autenticazione JWT │
|
||||
└──────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────────┐
|
||||
│ 2. Middleware GET (PATCH applicata) │
|
||||
│ → NON aggiunge più "Common" │
|
||||
│ → /scan usa SOLO req.user.name │
|
||||
└────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────┐
|
||||
│ 3. scanPhotosUser(user) │
|
||||
└────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 4. PRIMA PASSATA (ricorsiva) │
|
||||
│ countFilesUser() │
|
||||
│ → entra in tutte le sottocartelle │
|
||||
│ → conta SOLO i file supportati │
|
||||
│ → NON tocca DB │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 5. SECONDA PASSATA │
|
||||
│ per ogni cartella in /original: │
|
||||
│ → scanPhoto(cartella) │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 6. scanPhoto(cartella) │
|
||||
│ → chiama scanPhotoCartella() UNA volta │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 7. scanPhotoCartella() │
|
||||
│ │
|
||||
│ 7.1 Recupera ID dal DB → idsSet │
|
||||
│ 7.2 Scansiona file reali (scanCartella) │
|
||||
│ → scanPhotoSingle() │
|
||||
│ → idsSet.delete(id) │
|
||||
│ 7.3 ORFANI (solo alla fine) │
|
||||
│ → deleteThumbsById │
|
||||
│ → deleteFromDB │
|
||||
│ → recordChange("removed") │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────┐
|
||||
│ 8. scanPhoto ritorna newFiles │
|
||||
└────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────┐
|
||||
│ 9. scanPhotosUser aggrega tutti i newFiles │
|
||||
└──────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ 10. Risposta al client │
|
||||
└──────────────────────────┘
|
||||
|
||||
Perfetto Fabio — ora che abbiamo integrato **la cancellazione delle cartelle eliminate** e la funzione modulare `deleteFolderForUser()`, ti spiego **esattamente cosa fa /scan adesso**, passo per passo, in modo chiaro e completo.
|
||||
|
||||
Questa è la versione *vera*, aggiornata, precisa del comportamento attuale.
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **1. L’utente chiama `/scan`**
|
||||
|
||||
### ✔ Se è **Admin**
|
||||
- scansiona **Common**
|
||||
- scansiona **tutti gli utenti** presenti in `users.json`
|
||||
|
||||
### ✔ Se è **utente normale**
|
||||
- scansiona **solo il proprio spazio**
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **2. Dentro `scanPhotosUser(user)` succede questo**
|
||||
|
||||
## **Fase A — Lettura cartelle reali**
|
||||
Legge:
|
||||
|
||||
```
|
||||
/photos/<user>/original/
|
||||
```
|
||||
|
||||
e ottiene la lista delle cartelle *realmente presenti* sul filesystem.
|
||||
|
||||
Esempio:
|
||||
|
||||
```
|
||||
["Vacanze2023", "Lavoro", "Gatti"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **3. Rilevamento cartelle eliminate (NOVITÀ)**
|
||||
|
||||
Ora c’è la parte nuova e fondamentale:
|
||||
|
||||
### ✔ Recupera tutte le cartelle presenti nel DB per quell’utente
|
||||
(es. cartelle che contengono record)
|
||||
|
||||
### ✔ Confronta DB vs filesystem
|
||||
|
||||
Se nel DB c’è:
|
||||
|
||||
```
|
||||
["Vacanze2023", "Lavoro", "Gatti", "VecchiaCartella"]
|
||||
```
|
||||
|
||||
ma sul disco c’è:
|
||||
|
||||
```
|
||||
["Vacanze2023", "Lavoro", "Gatti"]
|
||||
```
|
||||
|
||||
allora:
|
||||
|
||||
```
|
||||
"VecchiaCartella" è stata cancellata dal filesystem
|
||||
```
|
||||
|
||||
### 👉 A questo punto viene chiamata:
|
||||
|
||||
```
|
||||
deleteFolderForUser(user, "VecchiaCartella")
|
||||
```
|
||||
|
||||
Che fa:
|
||||
|
||||
- elimina **tutti i record DB** della cartella
|
||||
- registra **removed** per ogni file
|
||||
- elimina **tutta la cartella thumbs residua**:
|
||||
|
||||
```
|
||||
/photos/<user>/thumbs/VecchiaCartella/
|
||||
```
|
||||
|
||||
### 🔥 Risultato:
|
||||
**DB pulito + thumbs puliti**
|
||||
anche se la cartella originale è stata cancellata manualmente.
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **4. Fase B — Conteggio file reali**
|
||||
|
||||
Viene eseguita la prima passata ricorsiva:
|
||||
|
||||
```
|
||||
countFilesUser()
|
||||
```
|
||||
|
||||
Serve solo per:
|
||||
|
||||
- contare i file reali
|
||||
- calcolare la percentuale
|
||||
- mostrare il progresso
|
||||
|
||||
Non modifica nulla.
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **5. Fase C — Scansione vera (UNA sola volta per cartella)**
|
||||
|
||||
Per ogni cartella reale:
|
||||
|
||||
```
|
||||
scanPhoto(cartella)
|
||||
```
|
||||
|
||||
Che:
|
||||
|
||||
- scansiona i file reali
|
||||
- aggiorna il DB
|
||||
- genera “added”
|
||||
- rileva orfani **solo dentro quella cartella**
|
||||
- elimina thumbs e DB per gli orfani
|
||||
- registra “removed”
|
||||
|
||||
---
|
||||
|
||||
# 🔵 **6. Fine scan**
|
||||
|
||||
Restituisce:
|
||||
|
||||
- lista dei nuovi file
|
||||
- log completo
|
||||
- DB aggiornato
|
||||
- thumbs aggiornati
|
||||
|
||||
---
|
||||
|
||||
# 🟢 **RIASSUNTO: COSA FA /scan ADESSO**
|
||||
|
||||
### ✔ Scansiona solo cartelle realmente presenti
|
||||
### ✔ Rileva cartelle eliminate dal filesystem
|
||||
### ✔ Cancella TUTTI i thumbs residui della cartella eliminata
|
||||
### ✔ Cancella TUTTI i record DB della cartella eliminata
|
||||
### ✔ Registra “removed” per ogni file eliminato
|
||||
### ✔ Scansiona ogni cartella UNA sola volta
|
||||
### ✔ Nessun doppio removed
|
||||
### ✔ Nessun orfano nascosto
|
||||
### ✔ Nessuna doppia passata
|
||||
### ✔ Nessun conflitto tra utenti
|
||||
### ✔ Admin può scansionare tutti
|
||||
|
||||
---
|
||||
|
||||
# 🔥 Vuoi che ti preparo anche un diagramma aggiornato del flusso `/scan` con la nuova logica di cancellazione cartelle?
|
||||
723
server.js
|
|
@ -1,296 +1,427 @@
|
|||
// 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);
|
||||
// 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 { handleAddFile, handleDeleteFile, handleAddDir, handleDeleteDir } = require('./api_v1/scanner/scanAuto.js');
|
||||
const { WEB_ROOT } = require('./api_v1/config');
|
||||
const config = require('./api_v1/config');
|
||||
|
||||
const db = require('./db/knex');
|
||||
// Funzioni di cleanup (deleteThumbsById, deleteFromDB)
|
||||
const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup');
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
// recordChange serve per registrare "removed"
|
||||
const recordChange = require('./api_v1/scanner/recordChange');
|
||||
|
||||
const photosRouter = require('./routes/photos');
|
||||
const wss = require('./ws-server');
|
||||
|
||||
|
||||
const scanFile = require('./api_v1/scanner/scanFileEntry');
|
||||
const scanPhotoSingle = require('./api_v1/scanner/scanPhotoSingle');
|
||||
const scanPhotosUser = require('./api_v1/scanner/scanPhotosUser');
|
||||
const createDeleteFolderFunctions = require('./api_v1/scanner/deleteFolder');
|
||||
const { deleteFolderForUser } = createDeleteFolderFunctions(db);
|
||||
|
||||
|
||||
|
||||
|
||||
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
|
||||
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
|
||||
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);
|
||||
}
|
||||
|
||||
function relPath2Cartella(p) {
|
||||
const parts = p.split("/").filter(Boolean);
|
||||
return parts.slice(3).join("/");
|
||||
}
|
||||
|
||||
// HOME
|
||||
server.get('/', (req, res) => {
|
||||
res.sendFile(path.resolve('public/index.html'));
|
||||
});
|
||||
|
||||
// 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' });
|
||||
}
|
||||
});
|
||||
|
||||
// ⭐ MIDDLEWARE GET — versione modulare
|
||||
server.use((req, res, next) => {
|
||||
if (req.method === 'GET' && req.user) {
|
||||
|
||||
// Admin non viene toccato
|
||||
if (req.user.name === 'Admin') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// NON applicare a /scan
|
||||
if (req.path.startsWith('/scan')) {
|
||||
req.query.user = [req.user.name];
|
||||
return next();
|
||||
}
|
||||
|
||||
// Per tutte le altre GET (lettura)
|
||||
req.query.user = [req.user.name, 'Common'];
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// ⭐ SCAN FOTO — Admin scansiona tutti
|
||||
server.get('/scan', async (req, res) => {
|
||||
try {
|
||||
if (req.user && req.user.name === 'Admin') {
|
||||
|
||||
// Admin scansiona Common
|
||||
await scanPhotosUser('Common', db);
|
||||
|
||||
// Admin scansiona tutti gli utenti del file users.json
|
||||
for (const u of userdb.users) {
|
||||
await scanPhotosUser(u.name, db);
|
||||
}
|
||||
|
||||
return res.send({
|
||||
status: 'Scansione completata',
|
||||
user: 'Admin',
|
||||
scope: 'tutti gli utenti + Common',
|
||||
});
|
||||
}
|
||||
|
||||
// Utente normale → solo se stesso
|
||||
await scanPhotosUser(req.user.name, db);
|
||||
|
||||
res.send({
|
||||
status: 'Scansione completata',
|
||||
user: req.user.name,
|
||||
scope: 'utente corrente',
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore scan:', err);
|
||||
res.status(500).json({ error: 'Errore durante lo scan', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// scan_auto new
|
||||
server.post('/api_v1/auto_scan', async (req, res) => {
|
||||
const { type, file, path: relPath, user } = req.body;
|
||||
const cart = relPath2Cartella(relPath);
|
||||
const absFile = path.join(__dirname, WEB_ROOT, relPath, file);
|
||||
|
||||
/*console.log("type: ", type);
|
||||
console.log("file: ", file);
|
||||
console.log("relPath: ", relPath);
|
||||
console.log("user: ", user);
|
||||
console.log("cart: ", cart);
|
||||
console.log("absFile: ", absFile);
|
||||
return res.json({ status: 'OK', action: 'DEL_DIR', folder: "prova" });
|
||||
*/
|
||||
try {
|
||||
switch (type) {
|
||||
|
||||
// -------------------------
|
||||
// ADD_DIR
|
||||
// -------------------------
|
||||
case 'ADD_DIR': {
|
||||
const { scanNewCartella } = require('./api_v1/scanner/scanNewCartella');
|
||||
|
||||
const result = await scanNewCartella(user, file, db);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "add_dir",
|
||||
folder: file
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'ADD_DIR', result });
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// DEL (file)
|
||||
// -------------------------
|
||||
case 'DEL': {
|
||||
const e = await scanFile(user, cart, absFile);
|
||||
|
||||
await recordChange(db, e.id, user, "removed");
|
||||
await deleteThumbsById(e.id);
|
||||
await deleteFromDB(e.id, user);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "removed",
|
||||
id: e.id
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'DEL', id: e.id });
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// ADD (file)
|
||||
// -------------------------
|
||||
case 'ADD': {
|
||||
const f = await scanFile(user, cart, absFile);
|
||||
const newFiles = [];
|
||||
|
||||
await scanPhotoSingle(db, user, cart, f, newFiles);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "added",
|
||||
id: f.id
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'ADD', id: f.id });
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// DEL_DIR
|
||||
// -------------------------
|
||||
case 'DEL_DIR': {
|
||||
await deleteFolderForUser(user, file);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "del_dir",
|
||||
folder: file
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'DEL_DIR', folder: cart });
|
||||
}
|
||||
|
||||
default:
|
||||
return res.status(400).json({ error: 'Tipo non valido' });
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore scan_auto:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 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);
|
||||
763
server.js.ok
|
|
@ -1,296 +1,467 @@
|
|||
// 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);
|
||||
// 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 { handleAddFile, handleDeleteFile, handleAddDir, handleDeleteDir } = require('./api_v1/scanner/scanAuto.js');
|
||||
const { WEB_ROOT } = require('./api_v1/config');
|
||||
const config = require('./api_v1/config');
|
||||
|
||||
const db = require('./db/knex');
|
||||
// Funzioni di cleanup (deleteThumbsById, deleteFromDB)
|
||||
const createCleanupFunctions = require('./api_v1/scanner/orphanCleanup');
|
||||
const { deleteThumbsById, deleteFromDB } = createCleanupFunctions(db);
|
||||
|
||||
// recordChange serve per registrare "removed"
|
||||
const recordChange = require('./api_v1/scanner/recordChange');
|
||||
|
||||
const photosRouter = require('./routes/photos');
|
||||
const wss = require('./ws-server');
|
||||
|
||||
|
||||
const scanFile = require('./api_v1/scanner/scanFileEntry');
|
||||
const scanPhotoSingle = require('./api_v1/scanner/scanPhotoSingle');
|
||||
const scanPhotosUser = require('./api_v1/scanner/scanPhotosUser');
|
||||
const createDeleteFolderFunctions = require('./api_v1/scanner/deleteFolder');
|
||||
const { deleteFolderForUser } = createDeleteFolderFunctions(db);
|
||||
|
||||
|
||||
|
||||
|
||||
const SECRET_KEY = process.env.JWT_SECRET || '123456789';
|
||||
const EXPIRES_IN = process.env.JWT_EXPIRES || '1h';
|
||||
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);
|
||||
}
|
||||
|
||||
function relPath2Cartella(p) {
|
||||
const parts = p.split("/").filter(Boolean);
|
||||
return parts.slice(3).join("/");
|
||||
}
|
||||
|
||||
// HOME
|
||||
server.get('/', (req, res) => {
|
||||
res.sendFile(path.resolve('public/index.html'));
|
||||
});
|
||||
|
||||
// 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' });
|
||||
}
|
||||
});
|
||||
|
||||
// ⭐ MIDDLEWARE GET — versione modulare
|
||||
server.use((req, res, next) => {
|
||||
if (req.method === 'GET' && req.user) {
|
||||
|
||||
// Admin non viene toccato
|
||||
if (req.user.name === 'Admin') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// NON applicare a /scan
|
||||
if (req.path.startsWith('/scan')) {
|
||||
req.query.user = [req.user.name];
|
||||
return next();
|
||||
}
|
||||
|
||||
// Per tutte le altre GET (lettura)
|
||||
req.query.user = [req.user.name, 'Common'];
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// ⭐ SCAN FOTO — Admin scansiona tutti
|
||||
server.get('/scan', async (req, res) => {
|
||||
try {
|
||||
if (req.user && req.user.name === 'Admin') {
|
||||
|
||||
// Admin scansiona Common
|
||||
await scanPhotosUser('Common', db);
|
||||
|
||||
// Admin scansiona tutti gli utenti del file users.json
|
||||
for (const u of userdb.users) {
|
||||
await scanPhotosUser(u.name, db);
|
||||
}
|
||||
|
||||
return res.send({
|
||||
status: 'Scansione completata',
|
||||
user: 'Admin',
|
||||
scope: 'tutti gli utenti + Common',
|
||||
});
|
||||
}
|
||||
|
||||
// Utente normale → solo se stesso
|
||||
await scanPhotosUser(req.user.name, db);
|
||||
|
||||
res.send({
|
||||
status: 'Scansione completata',
|
||||
user: req.user.name,
|
||||
scope: 'utente corrente',
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore scan:', err);
|
||||
res.status(500).json({ error: 'Errore durante lo scan', details: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// scan_auto new
|
||||
server.post('/api_v1/auto_scan', async (req, res) => {
|
||||
const { type, file, path: relPath, user } = req.body;
|
||||
const cart = relPath2Cartella(relPath);
|
||||
const absFile = path.join(__dirname, WEB_ROOT, relPath, file);
|
||||
|
||||
/*console.log("type: ", type);
|
||||
console.log("file: ", file);
|
||||
console.log("relPath: ", relPath);
|
||||
console.log("user: ", user);
|
||||
console.log("cart: ", cart);
|
||||
console.log("absFile: ", absFile);
|
||||
return res.json({ status: 'OK', action: 'DEL_DIR', folder: "prova" });
|
||||
*/
|
||||
try {
|
||||
switch (type) {
|
||||
|
||||
// -------------------------
|
||||
// ADD_DIR
|
||||
// -------------------------
|
||||
case 'ADD_DIR': {
|
||||
const { scanNewCartella } = require('./api_v1/scanner/scanNewCartella');
|
||||
|
||||
const result = await scanNewCartella(user, file, db);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "add_dir",
|
||||
folder: file
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'ADD_DIR', result });
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// DEL (file)
|
||||
// -------------------------
|
||||
case 'DEL': {
|
||||
const e = await scanFile(user, cart, absFile);
|
||||
|
||||
await recordChange(db, e.id, user, "removed");
|
||||
await deleteThumbsById(e.id);
|
||||
await deleteFromDB(e.id, user);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "removed",
|
||||
id: e.id
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'DEL', id: e.id });
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// ADD (file)
|
||||
// -------------------------
|
||||
case 'ADD': {
|
||||
const f = await scanFile(user, cart, absFile);
|
||||
const newFiles = [];
|
||||
|
||||
await scanPhotoSingle(db, user, cart, f, newFiles);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "added",
|
||||
id: f.id
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'ADD', id: f.id });
|
||||
}
|
||||
|
||||
// -------------------------
|
||||
// DEL_DIR
|
||||
// -------------------------
|
||||
case 'DEL_DIR': {
|
||||
await deleteFolderForUser(user, file);
|
||||
|
||||
wss.broadcastToUser(user, {
|
||||
type: "del_dir",
|
||||
folder: file
|
||||
});
|
||||
|
||||
return res.json({ status: 'OK', action: 'DEL_DIR', folder: cart });
|
||||
}
|
||||
|
||||
default:
|
||||
return res.status(400).json({ error: 'Tipo non valido' });
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
console.error('Errore scan_auto:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
// scan automatico
|
||||
server.post("/api_v1/auto_scan2", async (req, res) => {
|
||||
const { type, file, path: relPath, user } = req.body;
|
||||
const cart = relPath2Cartella(relPath);
|
||||
const absFile = path.join(__dirname,WEB_ROOT,relPath,file);
|
||||
const e = await scanFile(user, cart, absFile);
|
||||
|
||||
const exists = await db('photos')
|
||||
.where({ id: e.id })
|
||||
.select(db.raw('1'))
|
||||
.first();
|
||||
|
||||
const found = !!exists;
|
||||
console.log("Trovato: ", found);
|
||||
console.log("=== SUPER_SCAN EVENT ===");
|
||||
console.log("TYPE:", type);
|
||||
console.log("FILE:", file);
|
||||
console.log("PATH:", relPath);
|
||||
console.log("CART", cart);
|
||||
console.log("ABSPATH:", __dirname);
|
||||
console.log("USER:", user);
|
||||
console.log("WEB_ROOT:", WEB_ROOT);
|
||||
console.log("absFike: ", absFile);
|
||||
console.log(e);
|
||||
console.log("========================");
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
server.post("/api_v1/auto_scan1", async (req, res) => {
|
||||
const { type, file, path: relPath, user } = req.body;
|
||||
const cart = relPath2Cartella(relPath);
|
||||
const absFile = path.join(__dirname,WEB_ROOT,relPath,file);
|
||||
|
||||
const f = await scanFile(user, cart, absFile);
|
||||
const newFiles = [];
|
||||
await scanPhotoSingle(db, user, cart, f, newFiles);
|
||||
console.log("Nuovo file", newFiles);
|
||||
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
// FILE STATICI
|
||||
server.get('/files', (req, res) => {
|
||||
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
|
|
@ -1,8 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
WATCH_DIR="./public/photos/Fabio"
|
||||
|
||||
echo "Monitoro con inotifywait: $WATCH_DIR"
|
||||
|
||||
# Avvia Node e gli passa gli eventi
|
||||
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" | node watcher_logic1.mjs Fabio
|
||||
22
w3.sh
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
USERS_FILE="./api_v1/users.json"
|
||||
|
||||
# Legge ogni utente dal JSON
|
||||
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
|
||||
|
||||
# Determina la cartella da monitorare
|
||||
if [[ "$user" == "Admin" ]]; then
|
||||
WATCH_DIR="./public/photos/Common/original"
|
||||
else
|
||||
WATCH_DIR="./public/photos/$user/original"
|
||||
fi
|
||||
|
||||
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
|
||||
|
||||
# Avvia watcher per ogni utente in background
|
||||
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
|
||||
| node watcher_logic3.mjs "$user" &
|
||||
done
|
||||
|
||||
wait
|
||||
22
w4.sh
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
USERS_FILE="./api_v1/users.json"
|
||||
|
||||
# Legge ogni utente dal JSON
|
||||
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
|
||||
|
||||
# Determina la cartella da monitorare
|
||||
if [[ "$user" == "Admin" ]]; then
|
||||
WATCH_DIR="./public/photos/Common/original"
|
||||
else
|
||||
WATCH_DIR="./public/photos/$user/original"
|
||||
fi
|
||||
|
||||
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
|
||||
|
||||
# Avvia watcher per ogni utente in background
|
||||
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
|
||||
| node watcher_logic4.mjs "$user" &
|
||||
done
|
||||
|
||||
wait
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
import fs from "fs";
|
||||
import readline from "readline";
|
||||
|
||||
const user = process.argv[2];
|
||||
const LOCK_FILE = `./${user}.scan`;
|
||||
const DELAY = 10000;
|
||||
|
||||
let lastEventTime = 0;
|
||||
let timer = null;
|
||||
|
||||
console.log(`Node attivo per utente: ${user}`);
|
||||
|
||||
function handleQuietPeriod() {
|
||||
const now = Date.now();
|
||||
if (now - lastEventTime < DELAY) return;
|
||||
|
||||
if (fs.existsSync(LOCK_FILE)) {
|
||||
console.log(`Lock ${LOCK_FILE} presente, attendo...`);
|
||||
timer = setTimeout(handleQuietPeriod, DELAY);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`modificato ${user} (nessuna attività per 10 secondi)`);
|
||||
fs.writeFileSync(LOCK_FILE, "lock");
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
rl.on("line", line => {
|
||||
console.log("Evento:", line);
|
||||
|
||||
lastEventTime = Date.now();
|
||||
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(handleQuietPeriod, DELAY);
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import fs from "fs";
|
||||
import readline from "readline";
|
||||
|
||||
const user = process.argv[2];
|
||||
|
||||
// Estensioni foto/video
|
||||
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
|
||||
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
|
||||
|
||||
// Funzione per capire se è foto o video
|
||||
function isMediaFile(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
return photoExt.some(ext => lower.endsWith(ext)) ||
|
||||
videoExt.some(ext => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
console.log(`Node attivo per utente: ${user}`);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
rl.on("line", line => {
|
||||
// line contiene: "path ACTION file"
|
||||
const parts = line.trim().split(" ");
|
||||
const file = parts[parts.length - 1];
|
||||
const path = parts[0];
|
||||
|
||||
if (isMediaFile(file)) {
|
||||
console.log(`Scan di ${user}: ${path}${file}`);
|
||||
} else {
|
||||
console.log(`Ignorato (non media): ${file}`);
|
||||
}
|
||||
});
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
import readline from "readline";
|
||||
|
||||
const user = process.argv[2];
|
||||
|
||||
// Estensioni foto/video
|
||||
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
|
||||
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
|
||||
|
||||
function isMediaFile(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
return photoExt.some(ext => lower.endsWith(ext)) ||
|
||||
videoExt.some(ext => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
console.log(`Node attivo per utente: ${user}`);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
rl.on("line", line => {
|
||||
// Esempio: "/path/dir/ CLOSE_WRITE,CLOSE file.jpg"
|
||||
const parts = line.trim().split(/\s+/);
|
||||
|
||||
const path = parts[0];
|
||||
const action = parts[1];
|
||||
const file = parts[2];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
if (!isMediaFile(file)) {
|
||||
console.log(`Ignorato (non media): ${file}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let type = null;
|
||||
|
||||
// ADD → solo CLOSE_WRITE o CLOSE_WRITE,CLOSE
|
||||
if (/^CLOSE_WRITE(,CLOSE)?$/.test(action)) {
|
||||
type = "ADD";
|
||||
}
|
||||
|
||||
// ADD → MOVED_TO (solo se esatto)
|
||||
else if (action === "MOVED_TO") {
|
||||
type = "ADD";
|
||||
}
|
||||
|
||||
// DEL → MOVED_FROM
|
||||
else if (action === "MOVED_FROM") {
|
||||
type = "DEL";
|
||||
}
|
||||
|
||||
// DEL → DELETE
|
||||
else if (action === "DELETE") {
|
||||
type = "DEL";
|
||||
}
|
||||
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${type} ${file} ${path} ${user}`);
|
||||
});
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
import readline from "readline";
|
||||
|
||||
const user = process.argv[2];
|
||||
|
||||
// Estensioni foto/video
|
||||
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
|
||||
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
|
||||
|
||||
function isMediaFile(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
return photoExt.some(ext => lower.endsWith(ext)) ||
|
||||
videoExt.some(ext => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
console.log(`Node attivo per utente: ${user}`);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
rl.on("line", line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
|
||||
const path = parts[0];
|
||||
const action = parts[1];
|
||||
const file = parts[2];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
const isDir = action.includes("ISDIR");
|
||||
|
||||
// 🔥 Se è directory → NON controllare mediafile
|
||||
if (!isDir && !isMediaFile(file)) {
|
||||
console.log(`Ignorato (non media): ${file}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let type = null;
|
||||
|
||||
//
|
||||
// 🔵 DIRECTORY EVENTS
|
||||
//
|
||||
|
||||
// ADD_DIR → MOVED_TO,ISDIR
|
||||
if (action === "MOVED_TO,ISDIR") {
|
||||
type = "ADD_DIR";
|
||||
}
|
||||
|
||||
// DEL_DIR → MOVED_FROM,ISDIR
|
||||
else if (action === "MOVED_FROM,ISDIR") {
|
||||
type = "DEL_DIR";
|
||||
}
|
||||
|
||||
//
|
||||
// 🔵 FILE EVENTS
|
||||
//
|
||||
|
||||
// ADD → CLOSE_WRITE o CLOSE_WRITE,CLOSE
|
||||
else if (/^CLOSE_WRITE(,CLOSE)?$/.test(action)) {
|
||||
type = "ADD";
|
||||
}
|
||||
|
||||
// ADD → MOVED_TO
|
||||
else if (action === "MOVED_TO") {
|
||||
type = "ADD";
|
||||
}
|
||||
|
||||
// DEL → MOVED_FROM
|
||||
else if (action === "MOVED_FROM") {
|
||||
type = "DEL";
|
||||
}
|
||||
|
||||
// DEL → DELETE
|
||||
else if (action === "DELETE") {
|
||||
type = "DEL";
|
||||
}
|
||||
|
||||
else {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`${type} ${file} ${path} ${user}`);
|
||||
});
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import readline from "readline";
|
||||
import fs from "fs";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const user = process.argv[2];
|
||||
|
||||
// ===============================
|
||||
// BASE_URL DAL .env
|
||||
// ===============================
|
||||
const BASE_URL = process.env.BASE_URL; // es: https://prova.patachina.it
|
||||
if (!BASE_URL) {
|
||||
console.error("ERRORE: BASE_URL non definita in .env");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// CREDENZIALI ADMIN
|
||||
// ===============================
|
||||
const adminSecret = JSON.parse(fs.readFileSync("./api_v1/admin_secret.json", "utf8"));
|
||||
let adminToken = null;
|
||||
let isRefreshing = false;
|
||||
|
||||
// ===============================
|
||||
// LOGIN ADMIN (fetch come frontend)
|
||||
// ===============================
|
||||
async function loginAdmin() {
|
||||
const res = await fetch(`${BASE_URL}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
email: adminSecret.email,
|
||||
password: adminSecret.password
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error("Errore login Admin:", await res.text());
|
||||
throw new Error("Login fallito");
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
adminToken = data.token;
|
||||
console.log("Watcher autenticato come Admin");
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// GARANTISCE TOKEN VALIDO
|
||||
// ===============================
|
||||
async function ensureAdminToken() {
|
||||
if (adminToken) return;
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise(resolve => {
|
||||
const interval = setInterval(() => {
|
||||
if (!isRefreshing) {
|
||||
clearInterval(interval);
|
||||
resolve();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
isRefreshing = true;
|
||||
await loginAdmin();
|
||||
isRefreshing = false;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// CHIAMATA A /auto_scan (fetch come frontend)
|
||||
// ===============================
|
||||
async function callAutoScan(type, file, path, user) {
|
||||
await ensureAdminToken();
|
||||
|
||||
const res = await fetch(`${BASE_URL}/api_v1/auto_scan`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer " + adminToken
|
||||
},
|
||||
body: JSON.stringify({ type, file, path, user })
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
console.log("Token scaduto, rinnovo…");
|
||||
adminToken = null;
|
||||
return callAutoScan(type, file, path, user);
|
||||
}
|
||||
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// ESTENSIONI MEDIA
|
||||
// ===============================
|
||||
const photoExt = [".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp", ".tiff", ".heic", ".heif"];
|
||||
const videoExt = [".mp4", ".mov", ".avi", ".mkv", ".webm", ".m4v"];
|
||||
|
||||
function isMediaFile(filename) {
|
||||
const lower = filename.toLowerCase();
|
||||
return photoExt.some(ext => lower.endsWith(ext)) ||
|
||||
videoExt.some(ext => lower.endsWith(ext));
|
||||
}
|
||||
|
||||
console.log(`Node attivo per utente: ${user}`);
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity
|
||||
});
|
||||
|
||||
// ===============================
|
||||
// LOGICA EVENTI
|
||||
// ===============================
|
||||
function startWatcher() {
|
||||
rl.on("line", line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
|
||||
const path = parts[0];
|
||||
const action = parts[1];
|
||||
const file = parts[2];
|
||||
|
||||
if (!file) return;
|
||||
|
||||
const isDir = action.includes("ISDIR");
|
||||
|
||||
if (!isDir && !isMediaFile(file)) {
|
||||
console.log(`Ignorato (non media): ${file}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let type = null;
|
||||
|
||||
if (action === "MOVED_TO,ISDIR") type = "ADD_DIR";
|
||||
else if (action === "MOVED_FROM,ISDIR") type = "DEL_DIR";
|
||||
else if (/^CLOSE_WRITE(,CLOSE)?$/.test(action)) type = "ADD";
|
||||
else if (action === "MOVED_TO") type = "ADD";
|
||||
else if (action === "MOVED_FROM") type = "DEL";
|
||||
else if (action === "DELETE") type = "DEL";
|
||||
else return;
|
||||
|
||||
console.log(`${type} ${file} ${path} ${user}`);
|
||||
|
||||
callAutoScan(type, file, path, user);
|
||||
});
|
||||
}
|
||||
|
||||
// ===============================
|
||||
// AVVIO: LOGIN + WATCHER
|
||||
// ===============================
|
||||
loginAdmin().then(() => {
|
||||
startWatcher();
|
||||
});
|
||||
22
ww.sh
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
USERS_FILE="./api_v1/users.json"
|
||||
|
||||
# Legge ogni utente dal JSON
|
||||
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
|
||||
|
||||
# Determina la cartella da monitorare
|
||||
if [[ "$user" == "Admin" ]]; then
|
||||
WATCH_DIR="./public/photos/Common/original"
|
||||
else
|
||||
WATCH_DIR="./public/photos/$user/original"
|
||||
fi
|
||||
|
||||
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
|
||||
|
||||
# Avvia watcher per ogni utente in background
|
||||
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
|
||||
| node watcher_logic1.mjs "$user" &
|
||||
done
|
||||
|
||||
wait
|
||||
22
www.sh
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
USERS_FILE="./api_v1/users.json"
|
||||
|
||||
# Legge ogni utente dal JSON
|
||||
for user in $(jq -r '.users[].name' "$USERS_FILE"); do
|
||||
|
||||
# Determina la cartella da monitorare
|
||||
if [[ "$user" == "Admin" ]]; then
|
||||
WATCH_DIR="./public/photos/Common/original"
|
||||
else
|
||||
WATCH_DIR="./public/photos/$user/original"
|
||||
fi
|
||||
|
||||
echo "Monitoro con inotifywait: $WATCH_DIR per utente $user"
|
||||
|
||||
# Avvia watcher per ogni utente in background
|
||||
inotifywait -m -r -e close_write,moved_to,moved_from,delete "$WATCH_DIR" \
|
||||
| node watcher_logic2.mjs "$user" &
|
||||
done
|
||||
|
||||
wait
|
||||