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