From 13e4c9d5d2c02fba4077363c2f55c9a87c37a614 Mon Sep 17 00:00:00 2001 From: Fabio Date: Thu, 26 Mar 2026 16:52:41 +0100 Subject: [PATCH] first commit --- .env | 1 + Dockerfile | 21 +++++ README.md | 11 +++ docker-compose.yml | 19 +++++ start.sh | 37 +++++++++ watcher_logic.mjs | 181 +++++++++++++++++++++++++++++++++++++++++++ watcher_logic.mjs.ok | 179 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 449 insertions(+) create mode 100644 .env create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100755 start.sh create mode 100644 watcher_logic.mjs create mode 100644 watcher_logic.mjs.ok diff --git a/.env b/.env new file mode 100644 index 0000000..8511d2c --- /dev/null +++ b/.env @@ -0,0 +1 @@ +BASE_URL=https://prova.patachina.it \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b6f67a9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y inotify-tools jq + +COPY watcher_logic.mjs /app/watcher_logic.mjs +COPY start.sh /app/start.sh +RUN chmod +x /app/start.sh + +# Installiamo dotenv +RUN npm install dotenv + +ENV NODE_ENV=production + +CMD ["/app/start.sh"] + + + + + diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c5cb12 --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# Watcher per galleria + +fare il setup su docker-compose.yml + +lanciare con + +``` +sudo docker compose up --build +``` + +è settata per la porta 4002 e in nginx ws-prova.patachina.it diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2455785 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.9" + +services: + watcher: + build: . + container_name: watcher_multi + env_file: + - .env + volumes: + - /home/nvme/dockerdata/prove/s16j/public/photos:/app/photos + - /home/nvme/dockerdata/prove/s16j/api_v1:/app/config + restart: always + networks: + - fabio + +networks: + fabio: + external: true + diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..84245ed --- /dev/null +++ b/start.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +echo "== Avvio watcher multipli ==" + +CONFIG_FILE="/app/config/users.json" +PHOTOS_DIR="/app/photos" + +if [ ! -f "$CONFIG_FILE" ]; then + echo "ERRORE: $CONFIG_FILE non trovato" + exit 1 +fi + +USERS=$(jq -r '.users[].name' "$CONFIG_FILE") + +for USER in $USERS; do + + # 🔥 Regola speciale per Admin + if [ "$USER" = "Admin" ]; then + WATCH_DIR="$PHOTOS_DIR/Common/original" + else + WATCH_DIR="$PHOTOS_DIR/$USER/original" + fi + + if [ ! -d "$WATCH_DIR" ]; then + echo "Cartella non trovata per $USER: $WATCH_DIR" + continue + fi + + echo "Avvio watcher per $USER → $WATCH_DIR" + + sh -c "inotifywait -m -r -e close_write,delete,move --format '%w %e %f' \ + \"$WATCH_DIR\" | node /app/watcher_logic.mjs \"$USER\"" & +done + +echo "Tutti i watcher avviati." + +tail -f /dev/null diff --git a/watcher_logic.mjs b/watcher_logic.mjs new file mode 100644 index 0000000..3a6707a --- /dev/null +++ b/watcher_logic.mjs @@ -0,0 +1,181 @@ +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; +if (!BASE_URL) { + console.error("ERRORE: BASE_URL non definita in .env"); + process.exit(1); +} + +// =============================== +// CREDENZIALI ADMIN +// =============================== +const adminSecret = JSON.parse(fs.readFileSync("/app/config/admin_secret.json", "utf8")); +let adminToken = null; +let isRefreshing = false; + +// =============================== +// LOGIN ADMIN (solo Admin lo usa) +// =============================== +async function loginAdmin() { + while (true) { + try { + 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) { + const now = new Date().toISOString(); + console.error(`[${now}] [Admin] Login fallito (${res.status}). Riprovo tra 20 secondi...`); + await new Promise(r => setTimeout(r, 20000)); + continue; + } + + const data = await res.json(); + adminToken = data.token; + + const now = new Date().toISOString(); + console.log(`[${now}] Watcher autenticato come Admin`); + return; + + } catch (err) { + const now = new Date().toISOString(); + console.error(`[${now}] [Admin] Connessione al server fallita. Riprovo tra 20 secondi...`); + await new Promise(r => setTimeout(r, 20000)); + } + } +} + +// =============================== +// GARANTISCE TOKEN VALIDO (solo Admin) +// =============================== +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 +// =============================== +async function callAutoScan(type, file, path, user) { + + // Admin ottiene il token + // Gli altri utenti aspettano che Admin lo abbia + await ensureAdminToken(); + + const headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + adminToken // 🔥 tutti usano il token di Admin + }; + + path = path.replace(/^\/app/, ""); + + const res = await fetch(`${BASE_URL}/api_v1/auto_scan`, { + method: "POST", + headers, + body: JSON.stringify({ type, file, path, user }) + }); + + // Se il token scade, Admin lo rinnova + if (res.status === 401) { + console.log(`[${new Date().toISOString()}] 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: SOLO ADMIN FA LOGIN +// =============================== +if (user === "Admin") { + loginAdmin().then(() => { + startWatcher(); + }); +} else { + console.log(`Watcher per ${user} avviato senza login`); + startWatcher(); +} diff --git a/watcher_logic.mjs.ok b/watcher_logic.mjs.ok new file mode 100644 index 0000000..be1d2b1 --- /dev/null +++ b/watcher_logic.mjs.ok @@ -0,0 +1,179 @@ +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; +if (!BASE_URL) { + console.error("ERRORE: BASE_URL non definita in .env"); + process.exit(1); +} + +// =============================== +// CREDENZIALI ADMIN +// =============================== +const adminSecret = JSON.parse(fs.readFileSync("/app/config/admin_secret.json", "utf8")); +let adminToken = null; +let isRefreshing = false; + +// =============================== +// LOGIN ADMIN (solo Admin lo usa) +// =============================== +async function loginAdmin() { + while (true) { + try { + 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) { + const now = new Date().toISOString(); + console.error(`[${now}] [Admin] Login fallito (${res.status}). Riprovo tra 20 secondi...`); + await new Promise(r => setTimeout(r, 20000)); + continue; + } + + const data = await res.json(); + adminToken = data.token; + + const now = new Date().toISOString(); + console.log(`[${now}] Watcher autenticato come Admin`); + return; + + } catch (err) { + const now = new Date().toISOString(); + console.error(`[${now}] [Admin] Connessione al server fallita. Riprovo tra 20 secondi...`); + await new Promise(r => setTimeout(r, 20000)); + } + } +} + +// =============================== +// GARANTISCE TOKEN VALIDO (solo Admin) +// =============================== +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 +// =============================== +async function callAutoScan(type, file, path, user) { + + // Admin ottiene il token + // Gli altri utenti aspettano che Admin lo abbia + await ensureAdminToken(); + + const headers = { + "Content-Type": "application/json", + "Authorization": "Bearer " + adminToken // 🔥 tutti usano il token di Admin + }; + + const res = await fetch(`${BASE_URL}/api_v1/auto_scan`, { + method: "POST", + headers, + body: JSON.stringify({ type, file, path, user }) + }); + + // Se il token scade, Admin lo rinnova + if (res.status === 401) { + console.log(`[${new Date().toISOString()}] 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: SOLO ADMIN FA LOGIN +// =============================== +if (user === "Admin") { + loginAdmin().then(() => { + startWatcher(); + }); +} else { + console.log(`Watcher per ${user} avviato senza login`); + startWatcher(); +}