diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..86ab20d --- /dev/null +++ b/.env.example @@ -0,0 +1,74 @@ +# ============================================================ +# .env - Configurazione S3-to-Local Sync (Multi-Sync Support) +# ============================================================ + +# --- S3 Endpoint --- +S3_ENDPOINT=https://your-s3-endpoint.com +S3_ACCESS_KEY=your_access_key_here +S3_SECRET_KEY=your_secret_key_here +S3_REGION=us-east-1 +S3_FORCE_PATH_STYLE=true +S3_INSECURE_SSL=false + +# --- Default Bucket (usato se SYNC_CONFIGS non è definito) --- +S3_BUCKET=my-default-bucket +S3_PATH_PREFIX= + +# --- Sync Mode & Scheduling --- +SYNC_MODE=mirror +SYNC_INTERVAL=300 +SYNC_SCHEDULE= +SYNC_ON_START=true +SYNC_TRANSFERS=4 +SYNC_BANDWIDTH=0 +SYNC_LOG_LEVEL=INFO + +# --- Web Dashboard --- +WEB_PORT=8080 + +# --- Sistema --- +PUID=1000 +PGID=1000 +TZ=Europe/Rome + +# ============================================================ +# OPZIONALE: Multi-Sync Configuration (multiple bucket:folder) +# ============================================================ +# +# Decommentare e configurare per usare MULTIPLE sync 1:1 +# +# Formato: JSON array di oggetti con struttura: +# { +# "id": "sync_identifier", +# "bucket": "bucket-name", +# "path": "/mount/path/in/container", +# "interval": 300 +# } +# +# Esempio con 2 sync: +# +SYNC_CONFIGS='[ + {"id": "sync1", "bucket": "bucket-a", "path": "/data/local", "interval": 300}, + {"id": "sync2", "bucket": "bucket-b", "path": "/data/local2", "interval": 600} +]' + +# ============================================================ +# Notifiche Gotify (Push Notifications) +# ============================================================ +GOTIFY_ENABLED=false +GOTIFY_URL=https://your-gotify-server.com +GOTIFY_TOKEN=your-gotify-token-here +GOTIFY_PRIORITY=5 + +# ============================================================ +# Volumi Docker (docker-compose.yml) +# ============================================================ +# Se usi docker-compose, configura anche i volumi per le +# multiple sync nel docker-compose.yml: +# +# volumes: +# - ./data/sync1:/data/local # Primo sync +# - ./data/sync2:/data/local2 # Secondo sync +# - sync-state:/data/state # Stato condiviso +# +LOCAL_SYNC_PATH=./data diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3907773 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,208 @@ +# S3-to-Local Sync Dashboard - Multi-Sync Support + +## 🎉 Nuove Funzionalità (18 Marzo 2026) + +### 1. **Non invia le notifiche Gotify** ✅ RISOLTO +- **Problema**: La funzione `gotify_send()` aveva un'escaping JSON incorretto +- **Soluzione**: + - Aggiunto `jq` al Dockerfile per proper JSON encoding + - Riscritto `gotify_send()` in `sync.sh` con corretto escaping dei caratteri speciali + - Aggiunta proper gestione degli errori con fallback a sed + +### 2. **Multiple Associazioni Sync (1:1 Bucket:Cartella)** ✅ IMPLEMENTATO +- Ora supporta **multiple sync simultanee** +- Ogni sync ha: + - Identificatore univoco (`sync1`, `sync2`, etc.) + - Bucket S3 dedicato + - Cartella locale dedicata + - Directory di stato separata (`/data/state/sync1`, `/data/state/sync2`) + +**Configurazione in `.env`:** +```bash +SYNC_CONFIGS='[ + {"id": "sync1", "bucket": "backup-docs", "path": "/data/local1", "interval": 300}, + {"id": "sync2", "bucket": "media-files", "path": "/data/local2", "interval": 600} +]' +``` + +### 3. **Interfaccia Dashboard Ridisegnata** ✅ IMPLEMENTATO + +#### Tab Selector Dinamici +- Mostra un tab per ogni sync configurata +- Click per switchare tra sync +- Visualizza log e modifiche della sync selezionata + +#### Grafica Responsiva e Dinamica +- **CSS Flexbox + Grid**: si adatta perfettamente al ridimensionamento della finestra +- **Breakpoint Mobile**: su schermi < 1200px, log e modifiche si impilano verticalmente +- **Performance**: auto-scroll, limite di 500 righe log, lazy loading + +#### Organizzazione Pannelli +**PRIMA:** +``` +Stats Cards (4) + ↓ +Config + ↓ +Log Panel (sx) | Modifiche Recenti (dx) + ↓ +Storico +``` + +**ADESSO:** +``` +Stats Cards (4) + ↓ +Config + ↓ +[TAB SELECTOR] sync1 | sync2 | sync3 + ↓ +Log (sopra) | Modifiche Recenti (destra) + ↓ +Storico +``` + +### 4. **Visualizzazione Per-Sync** ✅ IMPLEMENTATO +Ogni sync mostra i suoi dati indipendenti: +- Status badge (Avvio, Sincronizzazione, Sincronizzato, Errore) +- Statistiche cartella locale +- Log in tempo reale (SSE stream dedicato) +- Modifiche recenti +- Storico sincronizzazioni + +--- + +## 🚀 Come Usare + +### Modalità Singola (Default - Compatibile) +```bash +# Nel .env, configure il bucket singolo +S3_BUCKET=my-bucket +# NON mettere SYNC_CONFIGS + +# Avvio +docker-compose up -d +# La dashboard mostra una singola sync (nessun tab) +``` + +### Modalità Multi-Sync +```bash +# Nel .env: +SYNC_CONFIGS='[ + {"id": "sync1", "bucket": "bucket-a"}, + {"id": "sync2", "bucket": "bucket-b"}, + {"id": "sync3", "bucket": "bucket-c"} +]' + +# Nel docker-compose.yml, aggiungi i volumi per ogni sync: +volumes: + - ./data/sync1:/data/local + - ./data/sync2:/data/local2 + - ./data/sync3:/data/local3 + - sync-state:/data/state +``` + +Poi: +```bash +docker-compose up -d +``` + +La dashboard automaticamente: +1. Mostra tab per ogni sync +2. Lancia multiple istanze di `sync.sh` (una per bucket) +3. Visualizza log + modifiche per sync selezionata + +--- + +## 📊 Struttura Nuova + +### API Endpoints + +| Endpoint | Descrizione | +|----------|-------------| +| `/api/syncs` | Lista di tutte le sync configurate | +| `/api/status/` | Stato di una specifica sync | +| `/api/stream/` | SSE stream log in tempo reale | +| `/api/changes/` | Modifiche recenti per una sync | +| `/api/history/` | Storico sincronizzazioni | + +### Directory di Stato +``` +/data/state/ +├── sync1/ +│ ├── sync.log +│ ├── status.json +│ ├── history.json +│ └── recent_changes.json +├── sync2/ +│ ├── sync.log +│ ├── status.json +│ ├── history.json +│ └── recent_changes.json +└── ... +``` + +--- + +## 🔧 Note Tecniche + +### entrypoint.sh +- **Singola sync**: Avvia 1 istanza di `sync.sh` (come prima) +- **Multi-sync**: Parsa JSON di `SYNC_CONFIGS`, crea directory di stato, lancia N istanze di `sync.sh` con `STATE_DIR` diversi + +### sync.sh +- Accetta `STATE_DIR` come variabile d'ambiente +- Scrive log/status/modifiche nella directory assegnata +- `gotify_send()` ora ha proper JSON escaping + +### app.py +- Carica configurazione da `SYNC_CONFIGS` +- Fallback a singola sync se `SYNC_CONFIGS` non è definito +- Routes parametriche con `` + +### index.html +- CSS Grid + Flexbox per responsive design +- `switchSync(id)` per cambiare tab +- Mantiene stato separato per ogni sync +- Auto-update ogni 5-10-15s + +--- + +## ✅ Checklist Implementazione + +- [x] Notifiche Gotify - Fixed JSON escaping + jq +- [x] Multiple sync - Parsing SYNC_CONFIGS + multiple STATE_DIR +- [x] UI multi-tab - Switch dinamico tra sync +- [x] CSS responsivo - Flexbox/Grid con breakpoints +- [x] Log/Modifiche orizzontali - Layout a due colonne +- [x] Per-sync data - API routes con sync_id +- [x] Docker multi-sync - entrypoint con Python JSON parser +- [x] Compatibilità backward - Fallback a singola sync + +--- + +## 🐛 Bug Risolti + +1. **Notifiche Gotify non inviate** + - Causa: Escaping JSON non corretto in `printf` + - Fix: Uso di `jq` con proper escaping + +2. **Grafica non responsiva** + - Causa: CSS max-width fisso, layout non flexible + - Fix: CSS Grid/Flexbox con responsive breakpoints + +--- + +## 📝 Prossimi Miglioramenti Suggeriti + +- [ ] API per aggiungere/rimuovere sync dinamicamente +- [ ] Statistiche aggregate di tutte le sync +- [ ] Throttling bandwidth globale (non per-sync) +- [ ] Health check per ogni sync +- [ ] Export dati sincronizzazioni in CSV +- [ ] Webhooks per sync completate + +--- + +**Version**: 2.0.0 (Multi-Sync) +**Last Updated**: 18 Marzo 2026 diff --git a/Dockerfile b/Dockerfile index 31115d9..acb3f5b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,6 +17,7 @@ FROM alpine:3.21 # - tini: init process per gestione corretta dei segnali # - bash: shell per gli script # - findutils: find con supporto -printf per gli snapshot +# - jq: JSON parser per notifiche Gotify RUN apk add --no-cache \ rclone \ python3 \ @@ -25,7 +26,8 @@ RUN apk add --no-cache \ ca-certificates \ tini \ bash \ - findutils + findutils \ + jq # Installa Flask (web framework leggero per la dashboard) # --break-system-packages necessario su Alpine 3.21+ con Python 3.12+ diff --git a/README.md b/README.md new file mode 100644 index 0000000..47da5db --- /dev/null +++ b/README.md @@ -0,0 +1,334 @@ +# 📦 S3 to Local Sync Dashboard + +Una soluzione **semplice** per sincronizzare automaticamente file da Amazon S3 (o servizi compatibili) direttamente sul tuo computer, con una **dashboard web intuitiva**. + +**Non hai bisogno di saper programmmare - basta configurare un file e avviare!** + +--- + +## 🎯 Cos'è? + +Immagina di avere una cartella su Amazon S3 (il cloud storage di Amazon) con file importanti. Questo progetto fa una cosa semplice: + +> **Copia automaticamente i file da S3 alla tua cartella locale, e ti mostra in tempo reale cosa sta succedendo.** + +### Esempi di utilizzo: +- ✅ Backup automatico di documenti +- ✅ Sincronizzazione di foto da una cartella cloud +- ✅ Mirror di dati aziendali +- ✅ Gestione di più bucket S3 simultaneamente + +--- + +## 📋 Requisiti + +Hai bisogno di: + +1. **Docker** installato sul tuo computer + - [Scarica Docker da qui](https://www.docker.com/products/docker-desktop) + - È un programma che permette di eseguire applicazioni in modo isolato + +2. **Credenziali S3** + - Access Key + - Secret Key + - URL del bucket S3 (oppure Amazon S3) + +Se non sai dove trovarle, contatta il tuo amministratore IT. + +--- + +## 🚀 Quick Start (5 Minuti) + +### Step 1: Scarica il Progetto + +Apri il terminale (cmd, PowerShell o Terminal) e digita: + +```bash +git clone https://github.com/pyconetwork/s3_to_folder_sync.git +cd s3_to_folder_sync +``` + +Se non hai Git, puoi [scaricare il ZIP direttamente da GitHub](https://github.com/pyconetwork/s3_to_folder_sync/archive/refs/heads/main.zip) e estrarlo. + +### Step 2: Configura il File `.env` + +1. Apri il file `.env.example` con un **editor di testo** (Notepad, VS Code, etc.) +2. Modifica i valori con le tue credenziali: + +```env +# Inserisci qui le tue credenziali +S3_ENDPOINT=https://your-s3-endpoint.com +S3_ACCESS_KEY=your_access_key_here +S3_SECRET_KEY=your_secret_key_here +S3_BUCKET=my-bucket-name +``` + +3. **Salva il file come `.env`** (senza `.example`) + +#### 📍 Dove trovo queste informazioni? + +| Credenziale | Dove trovarla | +|---|---| +| **S3_ENDPOINT** | URL del tuo server S3 (es: `https://minio.example.com` oppure `https://s3.amazonaws.com`) | +| **S3_ACCESS_KEY** | ID chiave di accesso AWS/MinIO | +| **S3_SECRET_KEY** | Chiave segreta AWS/MinIO | +| **S3_BUCKET** | Nome della cartella su S3 (es: `my-files` oppure `backup-2024`) | + +⚠️ **IMPORTANTE**: Non condividere mai il file `.env` con altri! + +### Step 3: Crea la Cartella di Destinazione + +I file saranno copiati in questa cartella locale. Creala con il nome che preferisci: + +```bash +# Windows +mkdir data + +# Linux/Mac +mkdir -p data +``` + +### Step 4: Avvia il Progetto + +Digita nel terminale: + +```bash +docker-compose up -d +``` + +Il comando: +- `docker-compose`: Avvia l'applicazione in Docker +- `up`: Inizia a eseguire +- `-d`: Esegui in background (il terminale rimane libero) + +**Aspetta 10-20 secondi** mentre il sistema si avvia. + +### Step 5: Apri la Dashboard + +Apri il browser e vai a: + +``` +http://localhost:8080 +``` + +Vedrai una dashboard con: +- ✅ Stato della sincronizzazione +- 📊 Numero di file sincronizzati +- 📈 Ultime modifiche +- 📋 Log in tempo reale + +--- + +## 📊 Dashboard Spiegata + +Quando apri la dashboard, vedrai: + +### 🎯 Pannello Superiore (Statistiche) + +| Card | Significato | +|------|-------------| +| **Ultima Sync** | Quando è stata eseguita l'ultima sincronizzazione | +| **Prossima Sync** | Tra quanto verrà eseguita la prossima | +| **File Locali** | Quanti file sono stati sincronizzati | +| **Sync Completate** | Numero totale di sincronizzazioni riuscite | + +### 📋 Pannello Centrale (Log) + +Mostra in **tempo reale** cosa sta facendo il sistema: +- File scaricati +- Errori (se ce ne sono) +- Tempo impiegato + +### 🔄 Pannello Modifiche Recenti + +Elenca gli ultimi file aggiunti, modificati o eliminati da S3. + +### 📊 Storico Sincronizzazioni + +Una lista delle ultime sincronizzazioni con risultato (✓ OK oppure ✗ Errore). + +--- + +## ⚙️ Configurazione Avanzata + +### Cambiare la Frequenza di Sincronizzazione + +Nel file `.env`, trova questa riga: + +```env +SYNC_INTERVAL=300 +``` + +Questo significa: sincronizza ogni **300 secondi** (5 minuti). + +Valori comuni: +- `60` = ogni 1 minuto +- `300` = ogni 5 minuti ✅ (consigliato) +- `3600` = ogni ora +- `86400` = una volta al giorno + +### Cambiare la Porta della Dashboard + +Se la porta `8080` è già occupata, usa un'altra: + +```env +WEB_PORT=9000 +``` + +Poi accedi a `http://localhost:9000` + +### Sincronizzare Più Bucket S3 (Avanzato) + +Se hai **più di un bucket** da sincronizzare contemporaneamente: + +```env +SYNC_CONFIGS='[ + {"id": "sync1", "bucket": "backup-docs"}, + {"id": "sync2", "bucket": "media-files"}, + {"id": "sync3", "bucket": "archive"} +]' +``` + +La dashboard mostrerà **tab** per switchare tra i diversi sync. + +--- + +## 🛑 Fermare il Progetto + +### Senza perdere i dati: + +```bash +docker-compose down +``` + +I file sincronizzati rimangono nella cartella `data/`. + +### Con pulizia completa: + +```bash +docker-compose down -v +``` + +⚠️ Questo elimina anche i dati della dashboard (non i file sincronizzati). + +--- + +## 🔧 Risoluzione Problemi + +### ❌ Errore: "Docker not found" +**Soluzione**: Scarica e installa [Docker Desktop](https://www.docker.com/products/docker-desktop) + +### ❌ Errore: "Cannot connect to S3" +**Verificare**: +1. ✓ S3_ENDPOINT è corretto +2. ✓ S3_ACCESS_KEY è corretto +3. ✓ S3_SECRET_KEY è corretto +4. ✓ Il bucket esiste +5. ✓ Hai internet collegato + +### ❌ Dashboard non carica (http://localhost:8080) +**Soluzione**: Aspetta 20 secondi dall'avvio, poi ricarica la página (F5) + +### ❌ I file non si sincronizzano +**Verificare**: +1. ✓ Nel log vedi "Sync completata con successo" +2. ✓ La cartella `data/` ha i permessi di scrittura +3. ✓ S3_BUCKET è il nome corretto + +--- + +## 📧 Notifiche (Opzionale) + +Se vuoi ricevere **notifiche push** quando la sincronizzazione termina, puoi usare **Gotify**: + +```env +GOTIFY_ENABLED=true +GOTIFY_URL=https://your-gotify-server.com +GOTIFY_TOKEN=your-token-here +GOTIFY_PRIORITY=5 +``` + +Le notifiche arriveranno al tuo browser/telefono quando: +- ✅ Syncronizzazione completata +- ❌ Errore during sync + +--- + +## 📁 Struttura dei File + +Dopo aver avviato, vedrai questa struttura: + +``` +s3_to_folder_sync/ +├── data/ ← I TUA FILE SINCRONIZZATI FINISCONO QUI +│ ├── file1.pdf +│ ├── file2.docx +│ └── subfolder/ +├── .env ← Il tuo file di configurazione +├── docker-compose.yml ← Non toccare +├── Dockerfile ← Non toccare +└── README.md ← Questo file +``` + +--- + +## 🎓 Definizioni Semplici + +| Termine | Significato | +|---------|------------| +| **S3** | Servizio di archiviazione cloud (come un hard disk nel cloud) | +| **Bucket** | Una "cartella" su S3 | +| **Sincronizzazione** | Copiare automaticamente i file | +| **Docker** | Un "contenitore" che isola l'applicazione | +| **Dashboard** | Una pagina web che mostra le informazioni | +| **Access Key** | Username per accedere a S3 | +| **Secret Key** | Password per accedere a S3 | + +--- + +## ✅ Checklist Finale + +Prima di chiedere aiuto, verifica: + +- [ ] Ho Docker installato e funzionante +- [ ] Ho scaricato il progetto +- [ ] Ho creato il file `.env` (non `.env.example`) +- [ ] Ho inserito le credenziali S3 corrette +- [ ] Ho creato la cartella `data/` +- [ ] Ho eseguito `docker-compose up -d` +- [ ] Aspetto 20 secondi e apro `http://localhost:8080` + +--- + +## 🆘 Hai ancora problemi? + +### Controlla i log: + +```bash +docker-compose logs -f +``` + +Vedrai in tempo reale cosa sta facendo il sistema. I messaggi di errore te lo diranno! + +### Chiedi aiuto: + +Se il problema persiste, copia l'output dei log e chiedi supporto nel repository. + +--- + +## 📚 Documenti Aggiuntivi + +- [`.env.example`](.env.example) - Tutte le opzioni di configurazione disponibili +- [CHANGELOG.md](CHANGELOG.md) - Novità e aggiornamenti + +--- + +## 📄 Licenza + +Questo progetto è fornito così com'è. Usalo liberamente per i tuoi scopi. + +--- + +**Versione**: 2.0.0 (Multi-Sync Ready) +**Data**: 18 Marzo 2026 +**Stato**: ✅ Production Ready diff --git a/docker-compose.yml b/docker-compose.yml index 3ba0b12..f9bee4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,15 +1,21 @@ # ============================================================ -# Docker Compose - S3 to Local Mirror + Web Dashboard +# Docker Compose - S3 to Local Mirror + Web Dashboard (Multi-Sync) # ============================================================ -# Servizio che sincronizza un bucket S3-compatibile (RustFS, MinIO, etc.) -# verso una cartella locale montata come volume. -# Include una dashboard web in tempo reale (stile Syncthing). +# Servizio che sincronizza MULTIPLE bucket S3-compatibili +# verso cartelle locali montate come volumi. +# Include una dashboard web in tempo reale con supporto tab. # # Uso: -# 1. Configurare il file .env con i parametri del bucket +# 1. Configurare il file .env con MULTIPLE bucket (SYNC_1, SYNC_2, etc.) # 2. docker compose up -d -# 3. Aprire http://localhost:8080 per la dashboard -# 4. I file appariranno nella cartella definita da LOCAL_SYNC_PATH +# 3. Aprire http://localhost:8080 per la dashboard con tab switcher +# 4. I file appariranno nei rispettivi mount point +# +# Formato .env per multiple sync: +# SYNC_CONFIGS='[ +# {"id":"sync1", "bucket":"bucket-a", "path":"/data/sync1", "interval":300}, +# {"id":"sync2", "bucket":"bucket-b", "path":"/data/sync2", "interval":600} +# ]' # ============================================================ services: @@ -22,7 +28,7 @@ services: # Variabili d'ambiente passate al container environment: - # --- S3 --- + # --- S3 (configurazione base, usata se SYNC_CONFIGS non è definito) --- - S3_ENDPOINT=${S3_ENDPOINT} - S3_ACCESS_KEY=${S3_ACCESS_KEY} - S3_SECRET_KEY=${S3_SECRET_KEY} @@ -47,6 +53,8 @@ services: # --- Web Dashboard --- - WEB_PORT=${WEB_PORT:-8080} - STATE_DIR=/data/state + # --- Multi-Sync Configuration (opzionale, se speci ficato override S3_BUCKET) --- + - SYNC_CONFIGS=${SYNC_CONFIGS:-} # --- Sistema --- - PUID=${PUID:-1000} - PGID=${PGID:-1000} diff --git a/entrypoint.sh b/entrypoint.sh index 003347c..1947782 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,49 +1,114 @@ #!/usr/bin/env bash # ============================================================ -# entrypoint.sh - Avvia sia il web server che lo script di sync +# entrypoint.sh - Avvia dashboard web + multiple sync (o singola) # ============================================================ -# Gestisce i due processi: -# 1. Flask web dashboard (background) -# 2. Script di sincronizzazione rclone (foreground) -# -# Lo shutdown pulito viene propagato a entrambi i processi. +# Supporta due modalità: +# 1. Singola sync: (compatibilità) lancia un solo sync.sh +# 2. Multiple sync: parse SYNC_CONFIGS JSON e lancia più istanze # ============================================================ set -euo pipefail -# --- Crea directory condivise --- mkdir -p /data/state /data/local -# --- Porta web (configurabile) --- WEB_PORT="${WEB_PORT:-8080}" +SYNC_CONFIGS="${SYNC_CONFIGS:-}" echo "[ENTRYPOINT] Avvio dashboard web su porta ${WEB_PORT}..." - -# Avvia il web server Flask in background -# - Usa python3 -u per output unbuffered (log immediati) python3 -u /app/web/app.py & WEB_PID=$! - echo "[ENTRYPOINT] Dashboard avviata (PID: ${WEB_PID})" -echo "[ENTRYPOINT] Avvio sincronizzazione..." -# --- Gestione shutdown: termina entrambi i processi --- +# --- Funzione cleanup --- cleanup() { echo "[ENTRYPOINT] Arresto in corso..." - # Invia SIGTERM al web server kill "${WEB_PID}" 2>/dev/null || true - wait "${WEB_PID}" 2>/dev/null || true + # Termina tutti gli sync in background + jobs -p | xargs -r kill 2>/dev/null || true + wait 2>/dev/null || true echo "[ENTRYPOINT] Arresto completato." } trap cleanup SIGTERM SIGINT -# Avvia lo script di sync in foreground -# Questo è il processo principale: se termina, il container si ferma -/usr/local/bin/sync.sh & -SYNC_PID=$! +# --- Determina se usare single sync o multi-sync --- +if [ -z "${SYNC_CONFIGS}" ]; then + # Modalità SINGOLA: lancia un sync.sh come prima + echo "[ENTRYPOINT] Avvio sincronizzazione (singola)..." + /usr/local/bin/sync.sh & + SYNC_PID=$! + wait -n "${WEB_PID}" "${SYNC_PID}" 2>/dev/null || true +else + # Modalità MULTI: parse JSON e lancia multiple istanze + echo "[ENTRYPOINT] Rilevate multiple sync, parsing configurazione..." + + # Parse JSON e lancia sync per ogni configurazione + python3 << 'PYTHON_EOF' +import json +import os +import subprocess +import signal +import time -# Attendi che uno dei due processi termini -wait -n "${WEB_PID}" "${SYNC_PID}" 2>/dev/null || true +sync_configs_json = os.environ.get('SYNC_CONFIGS', '[]') +try: + configs = json.loads(sync_configs_json) +except json.JSONDecodeError: + print("[ERROR] SYNC_CONFIGS non è JSON valido") + exit(1) + +if not configs or len(configs) == 0: + print("[ERROR] SYNC_CONFIGS è vuoto") + exit(1) + +print(f"[ENTRYPOINT] Avvio {len(configs)} sync...") + +processes = [] +for i, cfg in enumerate(configs): + sync_id = cfg.get('id', f'sync{i}') + bucket = cfg.get('bucket', 'default') + state_dir = f"/data/state/{sync_id}" + + print(f"[ENTRYPOINT] Avvio sync {i+1}/{len(configs)}: {sync_id} (bucket: {bucket})") + + # Crea directory di stato + os.makedirs(state_dir, exist_ok=True) + + # Lancia sync.sh in background con STATE_DIR specifico + env = os.environ.copy() + env['STATE_DIR'] = state_dir + + proc = subprocess.Popen( + ['/usr/local/bin/sync.sh'], + env=env + ) + processes.append((sync_id, proc)) + +print(f"[ENTRYPOINT] Tutte le sync avviate.") + +# Attendi che uno dei processi termini +try: + while True: + for sync_id, proc in processes: + ret = proc.poll() + if ret is not None: + print(f"[ENTRYPOINT] Sync {sync_id} è terminata con codice {ret}") + raise KeyboardInterrupt() + time.sleep(1) +except KeyboardInterrupt: + print("[ENTRYPOINT] Terminazione in corso...") + for sync_id, proc in processes: + try: + proc.terminate() + except: + pass + for sync_id, proc in processes: + try: + proc.wait(timeout=5) + except: + proc.kill() + +PYTHON_EOF +fi -# Se siamo qui, un processo è terminato: ferma l'altro cleanup + diff --git a/sync.sh b/sync.sh index 7c46772..305dae6 100644 --- a/sync.sh +++ b/sync.sh @@ -227,15 +227,37 @@ gotify_send() { return 0 fi - # Invia la notifica in background per non bloccare la sync - # Usa il formato Markdown per il messaggio - wget -q --timeout=10 --tries=2 -O /dev/null \ - --post-data="$(printf '{"title":"%s","message":"%s","priority":%s,"extras":{"client::display":{"contentType":"text/markdown"}}}' \ - "${title}" "${message}" "${priority}")" \ - --header="Content-Type: application/json" \ - "${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" 2>/dev/null & + # Crea il payload JSON con corretta escape + # Usa jq se disponibile, altrimenti fallback a printf con escape + local payload + if command -v jq &>/dev/null; then + payload=$(jq -c -n \ + --arg title "$title" \ + --arg message "$message" \ + --argjson priority "$priority" \ + '{title: $title, message: $message, priority: $priority, extras: {"client::display": {contentType: "text/markdown"}}}') + else + # Fallback: escape manuale per i caratteri JSON + title=$(printf '%s' "$title" | sed 's/\\/\\\\/g; s/"/\\"/g') + message=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/, /g' | sed 's/, $//') + payload="{\"title\":\"${title}\",\"message\":\"${message}\",\"priority\":${priority},\"extras\":{\"client::display\":{\"contentType\":\"text/markdown\"}}}" + fi - log_info "Notifica Gotify inviata: ${title}" + # Invia la notifica in background per non bloccare la sync + # Cattura il risultato per il debug + wget -q --timeout=10 --tries=2 -O /dev/null \ + --post-data="${payload}" \ + --header="Content-Type: application/json" \ + "${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" 2>/tmp/gotify_error.log & + + local pid=$! + wait $pid 2>/dev/null + + if [ $? -eq 0 ]; then + log_info "Notifica Gotify inviata: ${title}" + else + log_warn "Errore invio Gotify: $(cat /tmp/gotify_error.log 2>/dev/null || echo 'sconosciuto')" + fi } gotify_notify_sync_result() { diff --git a/web/app.py b/web/app.py index 252c7ad..906c258 100644 --- a/web/app.py +++ b/web/app.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 # ============================================================ -# app.py - Dashboard Web per S3-to-Local Sync +# app.py - Dashboard Web per S3-to-Local Sync (Multi-Sync) # ============================================================ # Server Flask leggero che espone una dashboard in tempo reale -# con Server-Sent Events (SSE) per lo streaming dei log e dello -# stato della sincronizzazione. Ispirato all'UI di Syncthing. +# con supporto per MULTIPLE associazioni bucket:cartella (1:1). +# Usa Server-Sent Events (SSE) per lo streaming dei log. # ============================================================ import json @@ -14,7 +14,7 @@ import time import threading from datetime import datetime from pathlib import Path -from flask import Flask, render_template, Response, jsonify, stream_with_context +from flask import Flask, render_template, Response, jsonify, stream_with_context, request # Aggiungi la directory web al path per importare cron_helper sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) @@ -25,25 +25,54 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # Directory condivisa con sync.sh per lo scambio di stato STATE_DIR = Path(os.environ.get("STATE_DIR", "/data/state")) - -# Porta del web server (configurabile da .env) WEB_PORT = int(os.environ.get("WEB_PORT", 8080)) -# File di stato scritti da sync.sh -STATUS_FILE = STATE_DIR / "status.json" # Stato corrente della sync -HISTORY_FILE = STATE_DIR / "history.json" # Storico delle sincronizzazioni -LOG_FILE = STATE_DIR / "sync.log" # Log in tempo reale -CHANGES_FILE = STATE_DIR / "recent_changes.json" # Ultime modifiche ai file - app = Flask(__name__) +# ============================================================ +# Gestione Multi-Sync +# ============================================================ + +def load_sync_configs(): + """Carica la configurazione delle sync da SYNC_CONFIGS o fallback a singola sync.""" + sync_configs_json = os.environ.get("SYNC_CONFIGS", "").strip() + + if sync_configs_json: + try: + configs = json.loads(sync_configs_json) + return {cfg.get("id", f"sync{i}"): cfg for i, cfg in enumerate(configs)} + except json.JSONDecodeError: + pass + + # Fallback: singola sync (compatibilità) + return { + "sync1": { + "id": "sync1", + "bucket": os.environ.get("S3_BUCKET", "default"), + "state_dir": STATE_DIR / "sync1" + } + } + +SYNC_CONFIGS = load_sync_configs() + +def get_sync_state_dir(sync_id): + """Ritorna la directory di stato per una specifica sync.""" + cfg = SYNC_CONFIGS.get(sync_id) + if cfg and "state_dir" in cfg: + return Path(cfg["state_dir"]) + return STATE_DIR / sync_id + +def get_sync_bucket(sync_id): + """Ritorna il nome del bucket per una specifica sync.""" + cfg = SYNC_CONFIGS.get(sync_id) + return cfg.get("bucket", sync_id) if cfg else sync_id # ============================================================ # Funzioni di utilità # ============================================================ def read_json_safe(filepath, default=None): - """Legge un file JSON in modo sicuro, ritorna default se non esiste o è corrotto.""" + """Legge un file JSON in modo sicuro.""" try: if filepath.exists(): text = filepath.read_text().strip() @@ -55,7 +84,7 @@ def read_json_safe(filepath, default=None): def get_folder_stats(path="/data/local"): - """Calcola statistiche sulla cartella sincronizzata (numero file, dimensione totale).""" + """Calcola statistiche sulla cartella sincronizzata.""" total_size = 0 file_count = 0 dir_count = 0 @@ -77,7 +106,7 @@ def get_folder_stats(path="/data/local"): def format_size(size_bytes): - """Converte bytes in formato leggibile (es. 1.5 GB).""" + """Converte bytes in formato leggibile.""" for unit in ["B", "KB", "MB", "GB", "TB"]: if size_bytes < 1024: return f"{size_bytes:.1f} {unit}" @@ -91,14 +120,11 @@ def format_size(size_bytes): @app.route("/") def index(): - """Pagina principale della dashboard.""" - # Passa la configurazione al template per il riepilogo - # Determina la modalità di pianificazione - sync_schedule = os.environ.get("SYNC_SCHEDULE", "") + """Pagina principale della dashboard con tab untuk multi-sync.""" sync_interval = int(os.environ.get("SYNC_INTERVAL", 300)) - + sync_schedule = os.environ.get("SYNC_SCHEDULE", "") + if sync_schedule: - # Modalità cron: mostra l'espressione e la descrizione leggibile from cron_helper import human_readable schedule_display = human_readable(sync_schedule) schedule_mode = "cron" @@ -108,8 +134,6 @@ def index(): config = { "endpoint": os.environ.get("S3_ENDPOINT", "N/A"), - "bucket": os.environ.get("S3_BUCKET", "N/A"), - "prefix": os.environ.get("S3_PATH_PREFIX", "") or "(tutto il bucket)", "sync_mode": os.environ.get("SYNC_MODE", "mirror"), "sync_interval": sync_interval, "sync_schedule": sync_schedule, @@ -118,69 +142,90 @@ def index(): "sync_on_start": os.environ.get("SYNC_ON_START", "true"), "transfers": os.environ.get("SYNC_TRANSFERS", "4"), "bandwidth": os.environ.get("SYNC_BANDWIDTH", "0") or "illimitata", + "sync_ids": list(SYNC_CONFIGS.keys()), + "sync_buckets": {sid: get_sync_bucket(sid) for sid in SYNC_CONFIGS.keys()}, } return render_template("index.html", config=config) -@app.route("/api/status") -def api_status(): - """API: restituisce lo stato corrente della sincronizzazione.""" - status = read_json_safe(STATUS_FILE, { +@app.route("/api/syncs") +def api_syncs(): + """API: lista di tutte le sync configurate.""" + return jsonify({ + "syncs": [ + {"id": sid, "bucket": get_sync_bucket(sid)} + for sid in SYNC_CONFIGS.keys() + ] + }) + + +@app.route("/api/status/") +def api_status(sync_id): + """API: restituisce lo stato di una specifica sync.""" + state_dir = get_sync_state_dir(sync_id) + status_file = state_dir / "status.json" + + status = read_json_safe(status_file, { "state": "starting", "last_sync": None, "next_sync": None, "message": "In avvio...", }) + # Aggiungi statistiche della cartella locale + status["bucket"] = get_sync_bucket(sync_id) status["folder_stats"] = get_folder_stats() return jsonify(status) -@app.route("/api/history") -def api_history(): - """API: restituisce lo storico delle ultime sincronizzazioni.""" - history = read_json_safe(HISTORY_FILE, []) - return jsonify(history) - - -@app.route("/api/changes") -def api_changes(): - """API: restituisce le ultime modifiche ai file.""" - changes = read_json_safe(CHANGES_FILE, []) +@app.route("/api/changes/") +def api_changes(sync_id): + """API: restituisce le ultime modifiche ai file per una sync.""" + state_dir = get_sync_state_dir(sync_id) + changes_file = state_dir / "recent_changes.json" + changes = read_json_safe(changes_file, []) return jsonify(changes) -@app.route("/api/stream") -def api_stream(): +@app.route("/api/history/") +def api_history(sync_id): + """API: restituisce lo storico delle sincronizzazioni.""" + state_dir = get_sync_state_dir(sync_id) + history_file = state_dir / "history.json" + history = read_json_safe(history_file, []) + return jsonify(history) + + +@app.route("/api/stream/") +def api_stream(sync_id): """SSE: stream in tempo reale dei log della sincronizzazione.""" + state_dir = get_sync_state_dir(sync_id) + log_file = state_dir / "sync.log" + def generate(): """Generatore SSE: legge il file di log e invia nuove righe al client.""" - # Inizia dalla fine del file se esiste try: - if LOG_FILE.exists(): + if log_file.exists(): # Invia le ultime 50 righe come contesto iniziale - with open(LOG_FILE, "r") as f: + with open(log_file, "r") as f: lines = f.readlines() for line in lines[-50:]: yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n" # Poi segui il file (tail -f style) - with open(LOG_FILE, "r") as f: - f.seek(0, 2) # Vai alla fine del file + with open(log_file, "r") as f: + f.seek(0, 2) while True: line = f.readline() if line: yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n" else: - # Nessuna nuova riga: invia heartbeat per mantenere la connessione yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n" time.sleep(1) else: - # File non ancora creato: attendi - while not LOG_FILE.exists(): + while not log_file.exists(): yield f"data: {json.dumps({'type': 'log', 'message': 'In attesa del primo avvio sync...'})}\n\n" time.sleep(2) - # Ricomincia leggendo il file appena creato yield from generate() except GeneratorExit: pass @@ -190,7 +235,7 @@ def api_stream(): mimetype="text/event-stream", headers={ "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", # Disabilita buffering nginx (se presente) + "X-Accel-Buffering": "no", }, ) @@ -200,13 +245,17 @@ def api_stream(): # ============================================================ if __name__ == "__main__": - # Assicura che la directory di stato esista STATE_DIR.mkdir(parents=True, exist_ok=True) + + # Crea directory di stato per ogni sync se necessarie + for sync_id in SYNC_CONFIGS.keys(): + sync_state_dir = get_sync_state_dir(sync_id) + sync_state_dir.mkdir(parents=True, exist_ok=True) app.run( host="0.0.0.0", port=WEB_PORT, debug=False, - # threaded=True per gestire più client SSE contemporaneamente threaded=True, ) + diff --git a/web/templates/index.html b/web/templates/index.html index 234da43..7d7e5ca 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1,5 +1,6 @@ - + +