first commit
This commit is contained in:
commit
13e4c9d5d2
7 changed files with 449 additions and 0 deletions
1
.env
Normal file
1
.env
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
BASE_URL=https://prova.patachina.it
|
||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
|
|
@ -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"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
11
README.md
Normal file
11
README.md
Normal file
|
|
@ -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
|
||||||
19
docker-compose.yml
Normal file
19
docker-compose.yml
Normal file
|
|
@ -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
|
||||||
|
|
||||||
37
start.sh
Executable file
37
start.sh
Executable file
|
|
@ -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
|
||||||
181
watcher_logic.mjs
Normal file
181
watcher_logic.mjs
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
179
watcher_logic.mjs.ok
Normal file
179
watcher_logic.mjs.ok
Normal file
|
|
@ -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();
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue