From c06c916e1da576dcbd9991fba7e38a08d2e25469 Mon Sep 17 00:00:00 2001 From: Francesco Picone Date: Wed, 18 Mar 2026 16:57:46 +0100 Subject: [PATCH] ++ new project --- .dockerignore | 8 - .env.example | 78 ---- .gitignore | 9 - CHANGELOG.md | 208 --------- Dockerfile | 54 --- README.md | 334 -------------- credentials.json | 1 + docker-compose.yml | 95 ---- entrypoint.sh | 128 ------ sync.sh | 626 -------------------------- web/app.py | 310 ------------- web/cron_helper.py | 232 ---------- web/templates/index.html | 926 --------------------------------------- 13 files changed, 1 insertion(+), 3008 deletions(-) delete mode 100644 .dockerignore delete mode 100644 .env.example delete mode 100644 .gitignore delete mode 100644 CHANGELOG.md delete mode 100644 Dockerfile delete mode 100644 README.md create mode 100644 credentials.json delete mode 100644 docker-compose.yml delete mode 100644 entrypoint.sh delete mode 100644 sync.sh delete mode 100644 web/app.py delete mode 100644 web/cron_helper.py delete mode 100644 web/templates/index.html diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 085b07e..0000000 --- a/.dockerignore +++ /dev/null @@ -1,8 +0,0 @@ -# Non copiare nel container i file di configurazione e i dati -.env -.env.* -data/ -.git -.gitignore -README.md -LICENSE diff --git a/.env.example b/.env.example deleted file mode 100644 index d522b90..0000000 --- a/.env.example +++ /dev/null @@ -1,78 +0,0 @@ -# ============================================================ -# .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", -# "local_path": "/mount/path/in/container", -# "interval": 300 -# } -# -# Esempio con 2 sync: -# -SYNC_CONFIGS='[ - {"id": "sync1", "bucket": "bucket-a", "local_path": "/data/local1", "interval": 300}, - {"id": "sync2", "bucket": "bucket-b", "local_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/local1 # Primo sync -# - ./data/sync2:/data/local2 # Secondo sync -# - sync-state:/data/state # Stato condiviso -# -LOCAL_SYNC_PATH_1=./data/sync1 -LOCAL_SYNC_PATH_2=./data/sync2 - -# Fallback legacy (sync singola) -LOCAL_SYNC_PATH=./data diff --git a/.gitignore b/.gitignore deleted file mode 100644 index dd71762..0000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -# Dati sincronizzati -data/ - -# File di ambiente con credenziali -.env - -# File di sistema -.DS_Store -*.swp diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 3907773..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,208 +0,0 @@ -# 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 deleted file mode 100644 index acb3f5b..0000000 --- a/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -# ============================================================ -# Dockerfile - S3 to Local Sync + Web Dashboard -# ============================================================ -# Immagine leggera basata su Alpine con: -# - rclone: sincronizzazione da bucket S3-compatibili -# - Flask: dashboard web in tempo reale (stile Syncthing) -# - wget (incluso in BusyBox): notifiche push via Gotify -# ============================================================ - -FROM alpine:3.21 - -# Installa le dipendenze: -# - rclone: tool di sincronizzazione cloud storage -# - python3 + pip: per il web server Flask -# - tzdata: supporto timezone -# - ca-certificates: certificati SSL per connessioni HTTPS -# - 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 \ - py3-pip \ - tzdata \ - ca-certificates \ - tini \ - bash \ - findutils \ - jq - -# Installa Flask (web framework leggero per la dashboard) -# --break-system-packages necessario su Alpine 3.21+ con Python 3.12+ -RUN pip3 install --no-cache-dir --break-system-packages flask - -# Crea le directory di lavoro -RUN mkdir -p /data/local /data/state /app - -# Copia l'applicazione web -COPY web/ /app/web/ - -# Copia gli script -COPY sync.sh /usr/local/bin/sync.sh -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/sync.sh /usr/local/bin/entrypoint.sh - -# Porta esposta per la dashboard web (configurabile via WEB_PORT) -EXPOSE 8080 - -# Usa tini come init per gestire correttamente i segnali (SIGTERM, etc.) -ENTRYPOINT ["/sbin/tini", "--"] - -# Avvia l'entrypoint che gestisce sia web che sync -CMD ["/usr/local/bin/entrypoint.sh"] diff --git a/README.md b/README.md deleted file mode 100644 index 47da5db..0000000 --- a/README.md +++ /dev/null @@ -1,334 +0,0 @@ -# 📦 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/credentials.json b/credentials.json new file mode 100644 index 0000000..87a3baa --- /dev/null +++ b/credentials.json @@ -0,0 +1 @@ +{"url":"","accessKey":"jQUmwfPK63bSZeRGI9FM","secretKey":"KWyrdxcGM7PHowm4YlU3Na0AfXQEIinsSC6k5qhT","api":"s3v4","path":"auto"} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index f003844..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,95 +0,0 @@ -# ============================================================ -# Docker Compose - S3 to Local Mirror + Web Dashboard (Multi-Sync) -# ============================================================ -# 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 MULTIPLE bucket (SYNC_1, SYNC_2, etc.) -# 2. docker compose up -d -# 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", "local_path":"/data/local1", "interval":300}, -# {"id":"sync2", "bucket":"bucket-b", "local_path":"/data/local2", "interval":600} -# ]' -# ============================================================ - -services: - s3-sync: - build: - context: . - dockerfile: Dockerfile - container_name: s3-to-local-sync - restart: unless-stopped - - # Variabili d'ambiente passate al container - environment: - # --- 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} - - S3_BUCKET=${S3_BUCKET} - - S3_PATH_PREFIX=${S3_PATH_PREFIX:-} - - S3_REGION=${S3_REGION:-} - - S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE:-true} - - S3_INSECURE_SSL=${S3_INSECURE_SSL:-false} - # --- Sync / Pianificazione --- - - SYNC_INTERVAL=${SYNC_INTERVAL:-300} - - SYNC_SCHEDULE=${SYNC_SCHEDULE:-} - - SYNC_ON_START=${SYNC_ON_START:-true} - - SYNC_MODE=${SYNC_MODE:-mirror} - - SYNC_TRANSFERS=${SYNC_TRANSFERS:-4} - - SYNC_BANDWIDTH=${SYNC_BANDWIDTH:-0} - - SYNC_LOG_LEVEL=${SYNC_LOG_LEVEL:-INFO} - # --- Notifiche Gotify --- - - GOTIFY_ENABLED=${GOTIFY_ENABLED:-false} - - GOTIFY_URL=${GOTIFY_URL:-} - - GOTIFY_TOKEN=${GOTIFY_TOKEN:-} - - GOTIFY_PRIORITY=${GOTIFY_PRIORITY:-5} - # --- 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} - - TZ=${TZ:-Europe/Rome} - - # Volumi: - # - Cartelle locali per sync multiple (default) - # - Cartella locale legacy per sync singola - # - Volume per lo stato interno (persistente tra i restart) - volumes: - - ${LOCAL_SYNC_PATH_1:-./data/sync1}:/data/local1 - - ${LOCAL_SYNC_PATH_2:-./data/sync2}:/data/local2 - - ${LOCAL_SYNC_PATH:-./data}:/data/local - - sync-state:/data/state - - # Porta per la dashboard web - ports: - - "${WEB_PORT:-8080}:${WEB_PORT:-8080}" - - # Healthcheck: verifica che sia il sync che il web server siano attivi - healthcheck: - test: ["CMD-SHELL", "pgrep -f sync.sh && wget -q --spider http://localhost:${WEB_PORT:-8080}/ || exit 1"] - interval: 60s - timeout: 10s - retries: 3 - start_period: 30s - - # Limiti risorse (opzionali, decommentare se necessario) - # deploy: - # resources: - # limits: - # memory: 256M - # cpus: "0.5" - -# Volume per lo stato persistente della sync (storico, log, etc.) -volumes: - sync-state: - driver: local diff --git a/entrypoint.sh b/entrypoint.sh deleted file mode 100644 index 8dc35e8..0000000 --- a/entrypoint.sh +++ /dev/null @@ -1,128 +0,0 @@ -#!/usr/bin/env bash -# ============================================================ -# entrypoint.sh - Avvia dashboard web + multiple sync (o singola) -# ============================================================ -# 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 - -mkdir -p /data/state /data/local - -WEB_PORT="${WEB_PORT:-8080}" -SYNC_CONFIGS="${SYNC_CONFIGS:-}" - -echo "[ENTRYPOINT] Avvio dashboard web su porta ${WEB_PORT}..." -python3 -u /app/web/app.py & -WEB_PID=$! -echo "[ENTRYPOINT] Dashboard avviata (PID: ${WEB_PID})" - -# --- Funzione cleanup --- -cleanup() { - echo "[ENTRYPOINT] Arresto in corso..." - kill "${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 - -# --- 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 time - -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+1}') - bucket = cfg.get('bucket') - local_path = cfg.get('local_path') or cfg.get('path') or f'/data/local{i+1}' - interval = str(cfg.get('interval', os.environ.get('SYNC_INTERVAL', '300'))) - prefix = cfg.get('prefix', os.environ.get('S3_PATH_PREFIX', '')) - schedule = cfg.get('schedule', os.environ.get('SYNC_SCHEDULE', '')) - - if not bucket: - print(f"[ERROR] Config sync '{sync_id}' senza campo 'bucket'") - exit(1) - - state_dir = f"/data/state/{sync_id}" - - print(f"[ENTRYPOINT] Avvio sync {i+1}/{len(configs)}: {sync_id} (bucket: {bucket}, local: {local_path})") - - # Crea directory di stato - os.makedirs(state_dir, exist_ok=True) - os.makedirs(local_path, exist_ok=True) - - # Lancia sync.sh in background con STATE_DIR specifico - env = os.environ.copy() - env['STATE_DIR'] = state_dir - env['S3_BUCKET'] = bucket - env['LOCAL_PATH'] = local_path - env['SYNC_INTERVAL'] = interval - env['S3_PATH_PREFIX'] = prefix - env['SYNC_SCHEDULE'] = schedule - - 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 - -cleanup - diff --git a/sync.sh b/sync.sh deleted file mode 100644 index b27b65a..0000000 --- a/sync.sh +++ /dev/null @@ -1,626 +0,0 @@ -#!/usr/bin/env bash -# ============================================================ -# sync.sh - Script di sincronizzazione S3 → Locale -# ============================================================ -# Configura rclone dinamicamente dalle variabili d'ambiente -# e avvia un loop di sincronizzazione periodica. -# Scrive stato strutturato in /data/state/ per la dashboard web. -# ============================================================ - -set -euo pipefail - -# --- Colori per il log (solo su terminale, non nel file di log) --- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# --- Directory di stato condivisa con il web server --- -STATE_DIR="${STATE_DIR:-/data/state}" -LOG_FILE="${STATE_DIR}/sync.log" -STATUS_FILE="${STATE_DIR}/status.json" -HISTORY_FILE="${STATE_DIR}/history.json" -CHANGES_FILE="${STATE_DIR}/recent_changes.json" - -# Crea la directory di stato -mkdir -p "${STATE_DIR}" - -# Inizializza i file JSON se non esistono -[ -f "${HISTORY_FILE}" ] || echo '[]' > "${HISTORY_FILE}" -[ -f "${CHANGES_FILE}" ] || echo '[]' > "${CHANGES_FILE}" - -# --- Funzioni di logging --- -# Scrive sia su stdout (con colori) che sul file di log (senza colori) -log_info() { - local msg="[INFO] $(date '+%Y-%m-%d %H:%M:%S') - $*" - echo -e "${GREEN}${msg}${NC}" - echo "${msg}" >> "${LOG_FILE}" -} - -log_warn() { - local msg="[WARN] $(date '+%Y-%m-%d %H:%M:%S') - $*" - echo -e "${YELLOW}${msg}${NC}" - echo "${msg}" >> "${LOG_FILE}" -} - -log_error() { - local msg="[ERROR] $(date '+%Y-%m-%d %H:%M:%S') - $*" - echo -e "${RED}${msg}${NC}" - echo "${msg}" >> "${LOG_FILE}" -} - -# --- Funzioni di stato (scrivono JSON per la dashboard) --- -write_status() { - # Scrive lo stato corrente in formato JSON - local state="$1" - local message="${2:-}" - local now - now="$(date -u '+%Y-%m-%dT%H:%M:%SZ')" - - cat > "${STATUS_FILE}" </dev/null > "${output_file}" || true -} - -# Tronca il file di log se troppo grande (>5MB) -truncate_log_if_needed() { - if [ -f "${LOG_FILE}" ]; then - local size - size=$(stat -f%z "${LOG_FILE}" 2>/dev/null || stat -c%s "${LOG_FILE}" 2>/dev/null || echo 0) - if [ "${size}" -gt 5242880 ]; then - # Mantieni solo le ultime 1000 righe - tail -n 1000 "${LOG_FILE}" > "${LOG_FILE}.tmp" - mv "${LOG_FILE}.tmp" "${LOG_FILE}" - log_info "File di log troncato (superato 5MB)" - fi - fi -} - -# ============================================================ -# Notifiche Gotify -# ============================================================ -# Invia UNA SOLA notifica per ciclo di sync, al termine del -# processo, con riepilogo completo del lavoro eseguito. -# ============================================================ - -GOTIFY_ENABLED="${GOTIFY_ENABLED:-false}" -GOTIFY_URL="${GOTIFY_URL:-}" -GOTIFY_TOKEN="${GOTIFY_TOKEN:-}" -GOTIFY_PRIORITY="${GOTIFY_PRIORITY:-5}" - -gotify_send() { - # Invia una notifica push a Gotify - # Parametri: $1 = titolo, $2 = messaggio, $3 = priorità (opzionale) - local title="$1" - local message="$2" - local priority="${3:-${GOTIFY_PRIORITY}}" - - # Non inviare se Gotify è disabilitato o non configurato - if [ "${GOTIFY_ENABLED}" != "true" ] || [ -z "${GOTIFY_URL}" ] || [ -z "${GOTIFY_TOKEN}" ]; then - return 0 - fi - - # 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 - - # 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() { - # Notifica unica a fine processo di sync con riepilogo completo - # Parametri: $1 = success (true/false), $2 = durata, $3 = snap_after, $4 = snap_before - local success="$1" - local duration="$2" - local after_snap="${3:-}" - local before_snap="${4:-}" - - # Info pianificazione - local schedule_info - if [ "${SCHEDULE_MODE:-interval}" = "cron" ]; then - schedule_info="${SCHEDULE_HUMAN:-cron}" - else - schedule_info="ogni ${SYNC_INTERVAL}s" - fi - - if [ "${success}" = "true" ]; then - # --- Sync riuscita: calcola riepilogo modifiche --- - local summary - summary=$(python3 -c " -import sys - -def read_snapshot(path): - result = {} - try: - with open(path) as f: - for line in f: - line = line.strip() - if line: - parts = line.split('|', 2) - if len(parts) == 3: - result[parts[2]] = int(parts[0]) - except: - pass - return result - -def format_size(b): - for u in ['B','KB','MB','GB','TB']: - if b < 1024: return f'{b:.1f} {u}' - b /= 1024 - return f'{b:.1f} PB' - -before = read_snapshot('${before_snap}') -after = read_snapshot('${after_snap}') - -added = [p for p in after if p not in before] -modified = [p for p in after if p in before and after[p] != before[p]] -deleted = [p for p in before if p not in after] -total_size = sum(after.values()) - -lines = [] -if not added and not modified and not deleted: - lines.append('Nessuna modifica rilevata') -else: - if added: - lines.append(f'**+{len(added)}** nuovi') - for f in added[:3]: - lines.append(f' ↳ {f} ({format_size(after[f])})') - if len(added) > 3: - lines.append(f' ↳ ...e altri {len(added)-3}') - if modified: - lines.append(f'**~{len(modified)}** modificati') - for f in modified[:3]: - lines.append(f' ↳ {f}') - if len(modified) > 3: - lines.append(f' ↳ ...e altri {len(modified)-3}') - if deleted: - lines.append(f'**-{len(deleted)}** rimossi') - for f in deleted[:3]: - lines.append(f' ↳ {f}') - if len(deleted) > 3: - lines.append(f' ↳ ...e altri {len(deleted)-3}') - -lines.append(f'\\n**Totale locale:** {len(after)} file ({format_size(total_size)})') -print('\\\\n'.join(lines)) -" 2>/dev/null || echo "Riepilogo non disponibile") - - gotify_send \ - "✅ Sync #${SYNC_COUNT} completata" \ - "**Bucket:** ${S3_BUCKET}\\n**Modalità:** ${SYNC_MODE}\\n**Pianificazione:** ${schedule_info}\\n**Durata:** ${duration}s\\n\\n---\\n\\n${summary}" - - else - # --- Sync fallita: notifica con priorità alta --- - gotify_send \ - "❌ Sync fallita" \ - "**Bucket:** ${S3_BUCKET}\\n**Modalità:** ${SYNC_MODE}\\n**Pianificazione:** ${schedule_info}\\n**Durata:** ${duration}s\\n**Errori totali:** ${ERROR_COUNT}\\n\\nControllare i log per dettagli." \ - "8" - fi -} - -# --- Validazione variabili obbligatorie --- -log_info "Avvio S3-to-Local Sync..." -write_status "starting" "Validazione configurazione..." - -REQUIRED_VARS=("S3_ENDPOINT" "S3_ACCESS_KEY" "S3_SECRET_KEY" "S3_BUCKET") -for var in "${REQUIRED_VARS[@]}"; do - if [ -z "${!var:-}" ]; then - log_error "Variabile d'ambiente obbligatoria non impostata: $var" - write_status "error" "Variabile mancante: $var" - exit 1 - fi -done - -# --- Contatori --- -SYNC_COUNT=0 -ERROR_COUNT=0 -LAST_SYNC_TS="null" - -# --- Impostazione permessi utente (se specificati) --- -PUID="${PUID:-1000}" -PGID="${PGID:-1000}" -log_info "File verranno creati con UID=$PUID, GID=$PGID" - -# --- Configurazione rclone via environment (no file di config) --- -# Rclone supporta la configurazione tramite variabili d'ambiente -# con il prefisso RCLONE_CONFIG__ -export RCLONE_CONFIG_S3SOURCE_TYPE="s3" -export RCLONE_CONFIG_S3SOURCE_PROVIDER="Other" -export RCLONE_CONFIG_S3SOURCE_ENDPOINT="${S3_ENDPOINT}" -export RCLONE_CONFIG_S3SOURCE_ACCESS_KEY_ID="${S3_ACCESS_KEY}" -export RCLONE_CONFIG_S3SOURCE_SECRET_ACCESS_KEY="${S3_SECRET_KEY}" -export RCLONE_CONFIG_S3SOURCE_FORCE_PATH_STYLE="${S3_FORCE_PATH_STYLE:-true}" - -# Regione (opzionale) -if [ -n "${S3_REGION:-}" ]; then - export RCLONE_CONFIG_S3SOURCE_REGION="${S3_REGION}" -fi - -# Gestione SSL insicuro (per certificati self-signed in sviluppo) -if [ "${S3_INSECURE_SSL:-false}" = "true" ]; then - log_warn "SSL verification disabilitata - usare solo in sviluppo!" - export RCLONE_CONFIG_S3SOURCE_NO_CHECK_CERTIFICATE="true" -fi - -# --- Costruzione path sorgente --- -# Se S3_PATH_PREFIX è specificato, sincronizza solo quella sottocartella -SOURCE_PATH="S3SOURCE:${S3_BUCKET}" -if [ -n "${S3_PATH_PREFIX:-}" ]; then - SOURCE_PATH="${SOURCE_PATH}/${S3_PATH_PREFIX}" - log_info "Sincronizzazione limitata al prefisso: ${S3_PATH_PREFIX}" -fi - -# --- Parametri di sincronizzazione --- -SYNC_INTERVAL="${SYNC_INTERVAL:-300}" -SYNC_SCHEDULE="${SYNC_SCHEDULE:-}" -SYNC_MODE="${SYNC_MODE:-mirror}" -SYNC_TRANSFERS="${SYNC_TRANSFERS:-4}" -SYNC_BANDWIDTH="${SYNC_BANDWIDTH:-0}" -SYNC_LOG_LEVEL="${SYNC_LOG_LEVEL:-INFO}" -SYNC_ON_START="${SYNC_ON_START:-true}" - -LOCAL_PATH="${LOCAL_PATH:-/data/local}" -mkdir -p "${LOCAL_PATH}" - -# --- Helper cron (script Python per il parsing delle espressioni) --- -CRON_HELPER="/app/web/cron_helper.py" - -# --- Determina la modalità di scheduling --- -# Se SYNC_SCHEDULE è impostato, usa la pianificazione cron -# Altrimenti, usa SYNC_INTERVAL (comportamento classico) -if [ -n "${SYNC_SCHEDULE}" ]; then - SCHEDULE_MODE="cron" - # Valida l'espressione cron all'avvio - if ! python3 "${CRON_HELPER}" "${SYNC_SCHEDULE}" validate >/dev/null 2>&1; then - log_error "Espressione cron non valida: '${SYNC_SCHEDULE}'" - log_error "Formato: minuto ora giorno_mese mese giorno_settimana" - log_error "Esempi: '0 3 * * *' (ogni giorno alle 3:00), '*/30 * * * *' (ogni 30 min)" - write_status "error" "Espressione cron non valida: ${SYNC_SCHEDULE}" - exit 1 - fi - SCHEDULE_HUMAN=$(python3 "${CRON_HELPER}" "${SYNC_SCHEDULE}" human) - log_info "Pianificazione: CRON - ${SCHEDULE_HUMAN} (${SYNC_SCHEDULE})" -else - SCHEDULE_MODE="interval" - log_info "Pianificazione: INTERVALLO - ogni ${SYNC_INTERVAL}s" -fi - -# --- Determina il comando rclone in base alla modalità --- -case "${SYNC_MODE}" in - mirror) - # Mirror: replica esatta, cancella file locali non presenti nel bucket - RCLONE_CMD="sync" - log_info "Modalità: MIRROR (i file rimossi dal bucket verranno rimossi localmente)" - ;; - copy) - # Copy: solo copia, non cancella nulla localmente - RCLONE_CMD="copy" - log_info "Modalità: COPY (i file locali extra non verranno rimossi)" - ;; - *) - log_error "Modalità di sync non valida: ${SYNC_MODE} (valori ammessi: mirror, copy)" - write_status "error" "Modalità non valida: ${SYNC_MODE}" - exit 1 - ;; -esac - -# --- Riepilogo configurazione --- -log_info "=========================================" -log_info "Configurazione:" -log_info " Endpoint: ${S3_ENDPOINT}" -log_info " Bucket: ${S3_BUCKET}" -log_info " Prefisso: ${S3_PATH_PREFIX:-(nessuno, tutto il bucket)}" -log_info " Destinazione: ${LOCAL_PATH}" -log_info " Modalità: ${SYNC_MODE} (comando: rclone ${RCLONE_CMD})" -if [ "${SCHEDULE_MODE}" = "cron" ]; then - log_info " Pianificazione: ${SCHEDULE_HUMAN} (${SYNC_SCHEDULE})" -else - log_info " Intervallo: ${SYNC_INTERVAL}s" -fi -log_info " Sync all'avvio: ${SYNC_ON_START}" -log_info " Trasferimenti paralleli: ${SYNC_TRANSFERS}" -log_info " Limite banda: ${SYNC_BANDWIDTH:-nessuno}" -log_info " Log level: ${SYNC_LOG_LEVEL}" -if [ "${GOTIFY_ENABLED}" = "true" ]; then - log_info " Gotify: abilitato (${GOTIFY_URL}), priorità ${GOTIFY_PRIORITY}" -else - log_info " Gotify: disabilitato" -fi -log_info "=========================================" - -# --- Gestione segnali per shutdown pulito --- -RUNNING=true -trap 'log_info "Ricevuto segnale di stop, arresto..."; RUNNING=false' SIGTERM SIGINT - -# --- Funzione di sincronizzazione --- -do_sync() { - log_info "Inizio sincronizzazione..." - write_status "syncing" "Sincronizzazione in corso..." - - # Snapshot pre-sync per rilevare le modifiche - local before_snap="${STATE_DIR}/.snap_before" - local after_snap="${STATE_DIR}/.snap_after" - take_snapshot "${before_snap}" - - # Timestamp di inizio per calcolare la durata - local start_ts - start_ts=$(date +%s) - - # Costruisci gli argomenti rclone - RCLONE_ARGS=( - "${RCLONE_CMD}" - "${SOURCE_PATH}" - "${LOCAL_PATH}" - "--transfers" "${SYNC_TRANSFERS}" - "--log-level" "${SYNC_LOG_LEVEL}" - "--stats" "30s" # Mostra statistiche ogni 30s durante il trasferimento - "--stats-one-line" # Statistiche su una riga (più leggibile nei log) - "--no-update-modtime" # Non modifica i timestamp locali se il contenuto è uguale - "--log-file" "${LOG_FILE}" # Log anche su file per la dashboard - ) - - # Limite di banda (se specificato e diverso da 0) - if [ "${SYNC_BANDWIDTH}" != "0" ]; then - RCLONE_ARGS+=("--bwlimit" "${SYNC_BANDWIDTH}") - fi - - # Esegui rclone - local end_ts duration - if rclone "${RCLONE_ARGS[@]}"; then - end_ts=$(date +%s) - duration=$((end_ts - start_ts)) - - log_info "Sincronizzazione completata con successo in ${duration}s" - - # Aggiorna contatori - SYNC_COUNT=$((SYNC_COUNT + 1)) - LAST_SYNC_TS="\"$(date -u '+%Y-%m-%dT%H:%M:%SZ')\"" - - # Snapshot post-sync e calcolo differenze - take_snapshot "${after_snap}" - update_recent_changes "${after_snap}" "${before_snap}" - - # Notifica Gotify: unica notifica a fine sync con riepilogo - gotify_notify_sync_result "true" "${duration}" "${after_snap}" "${before_snap}" - - # Aggiungi allo storico - append_history "true" "${duration}" "0" "0 B" - - # Aggiorna proprietario dei file se necessario - chown -R "${PUID}:${PGID}" "${LOCAL_PATH}" 2>/dev/null || true - - write_status "idle" "Sincronizzazione completata" - else - end_ts=$(date +%s) - duration=$((end_ts - start_ts)) - - log_error "Sincronizzazione fallita dopo ${duration}s" - ERROR_COUNT=$((ERROR_COUNT + 1)) - - # Notifica Gotify: unica notifica di errore (priorità alta) - gotify_notify_sync_result "false" "${duration}" - - append_history "false" "${duration}" "0" "0 B" - write_status "error" "Sincronizzazione fallita" - fi - - # Pulizia snapshot temporanei - rm -f "${before_snap}" "${after_snap}" - - # Tronca log se troppo grande - truncate_log_if_needed -} - -# --- Loop principale --- - -# Esegui la prima sincronizzazione all'avvio (se abilitato) -if [ "${SYNC_ON_START}" = "true" ]; then - log_info "Sincronizzazione iniziale all'avvio..." - do_sync -else - log_info "Sync all'avvio disabilitata, in attesa della prossima pianificazione..." - write_status "waiting" "In attesa della prima esecuzione pianificata" -fi - -# Funzione per l'attesa interrompibile di N secondi -wait_seconds() { - local total="$1" - local waited=0 - while [ "$RUNNING" = true ] && [ "$waited" -lt "$total" ]; do - sleep 1 - waited=$((waited + 1)) - done -} - -# Loop in base alla modalità di scheduling -if [ "${SCHEDULE_MODE}" = "cron" ]; then - # ===== MODALITÀ CRON ===== - # Calcola i secondi fino alla prossima esecuzione cron e attende - - while [ "$RUNNING" = true ]; do - # Calcola prossima esecuzione - NEXT_RUN=$(python3 "${CRON_HELPER}" "${SYNC_SCHEDULE}" next) - WAIT_SECS=$(python3 "${CRON_HELPER}" "${SYNC_SCHEDULE}" seconds) - - log_info "Prossima esecuzione pianificata: ${NEXT_RUN} (tra ${WAIT_SECS}s)" - write_status "waiting" "Prossima sync: ${NEXT_RUN}" - - # Attesa interrompibile - wait_seconds "${WAIT_SECS}" - - # Esegui sync solo se siamo ancora in esecuzione - if [ "$RUNNING" = true ]; then - do_sync - fi - done -else - # ===== MODALITÀ INTERVALLO ===== - # Comportamento classico: attende SYNC_INTERVAL secondi tra le sync - - while [ "$RUNNING" = true ]; do - write_status "waiting" "Prossima sync tra ${SYNC_INTERVAL}s" - - # Attesa interrompibile - wait_seconds "${SYNC_INTERVAL}" - - # Esegui sync solo se siamo ancora in esecuzione - if [ "$RUNNING" = true ]; then - do_sync - fi - done -fi - -write_status "idle" "Sync arrestato" -log_info "Sync arrestato correttamente. Arrivederci!" diff --git a/web/app.py b/web/app.py deleted file mode 100644 index 0b8c230..0000000 --- a/web/app.py +++ /dev/null @@ -1,310 +0,0 @@ -#!/usr/bin/env python3 -# ============================================================ -# app.py - Dashboard Web per S3-to-Local Sync (Multi-Sync) -# ============================================================ -# Server Flask leggero che espone una dashboard in tempo reale -# con supporto per MULTIPLE associazioni bucket:cartella (1:1). -# Usa Server-Sent Events (SSE) per lo streaming dei log. -# ============================================================ - -import json -import os -import sys -import time -import threading -from datetime import datetime -from pathlib import Path -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__))) - -# ============================================================ -# Configurazione -# ============================================================ - -# Directory condivisa con sync.sh per lo scambio di stato -STATE_DIR = Path(os.environ.get("STATE_DIR", "/data/state")) -WEB_PORT = int(os.environ.get("WEB_PORT", 8080)) - -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) - parsed = {} - for i, cfg in enumerate(configs): - sync_id = cfg.get("id", f"sync{i+1}") - parsed[sync_id] = { - "id": sync_id, - "bucket": cfg.get("bucket", "default"), - "prefix": cfg.get("prefix", ""), - "local_path": cfg.get("local_path") or cfg.get("path") or f"/data/local{i+1}", - } - if parsed: - return parsed - except json.JSONDecodeError: - pass - - # Fallback: singola sync (compatibilità) - return { - "sync1": { - "id": "sync1", - "bucket": os.environ.get("S3_BUCKET", "default"), - "prefix": os.environ.get("S3_PATH_PREFIX", ""), - "local_path": os.environ.get("LOCAL_PATH", "/data/local"), - } - } - -SYNC_CONFIGS = load_sync_configs() - -def get_sync_state_dir(sync_id): - """Ritorna la directory di stato per una specifica sync.""" - 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 - -def get_sync_local_path(sync_id): - """Ritorna la cartella locale della sync specifica.""" - cfg = SYNC_CONFIGS.get(sync_id) - if cfg: - return cfg.get("local_path", "/data/local") - return "/data/local" - -def get_default_sync_id(): - """Ritorna il primo sync id disponibile.""" - if SYNC_CONFIGS: - return next(iter(SYNC_CONFIGS.keys())) - return "sync1" - -# ============================================================ -# Funzioni di utilità -# ============================================================ - -def read_json_safe(filepath, default=None): - """Legge un file JSON in modo sicuro.""" - try: - if filepath.exists(): - text = filepath.read_text().strip() - if text: - return json.loads(text) - except (json.JSONDecodeError, OSError): - pass - return default if default is not None else {} - - -def get_folder_stats(path="/data/local"): - """Calcola statistiche sulla cartella sincronizzata.""" - total_size = 0 - file_count = 0 - dir_count = 0 - try: - for entry in Path(path).rglob("*"): - if entry.is_file(): - file_count += 1 - total_size += entry.stat().st_size - elif entry.is_dir(): - dir_count += 1 - except OSError: - pass - return { - "file_count": file_count, - "dir_count": dir_count, - "total_size": total_size, - "total_size_human": format_size(total_size), - } - - -def format_size(size_bytes): - """Converte bytes in formato leggibile.""" - for unit in ["B", "KB", "MB", "GB", "TB"]: - if size_bytes < 1024: - return f"{size_bytes:.1f} {unit}" - size_bytes /= 1024 - return f"{size_bytes:.1f} PB" - - -# ============================================================ -# Routes -# ============================================================ - -@app.route("/") -def index(): - """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", "") - default_sync = get_default_sync_id() - default_cfg = SYNC_CONFIGS.get(default_sync, {}) - - if sync_schedule: - from cron_helper import human_readable - schedule_display = human_readable(sync_schedule) - schedule_mode = "cron" - else: - schedule_display = f"Ogni {sync_interval}s" - schedule_mode = "interval" - - config = { - "endpoint": os.environ.get("S3_ENDPOINT", "N/A"), - "bucket": default_cfg.get("bucket", os.environ.get("S3_BUCKET", "N/A")), - "prefix": default_cfg.get("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, - "schedule_mode": schedule_mode, - "schedule_display": schedule_display, - "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/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(get_sync_local_path(sync_id)) - return jsonify(status) - - -@app.route("/api/status") -def api_status_default(): - """Compat: stato della sync di default.""" - return api_status(get_default_sync_id()) - - -@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/changes") -def api_changes_default(): - """Compat: modifiche della sync di default.""" - return api_changes(get_default_sync_id()) - - -@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/history") -def api_history_default(): - """Compat: storico della sync di default.""" - return api_history(get_default_sync_id()) - - -@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.""" - try: - if log_file.exists(): - # Invia le ultime 50 righe come contesto iniziale - 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) - while True: - line = f.readline() - if line: - yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n" - else: - yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n" - time.sleep(1) - else: - while not log_file.exists(): - yield f"data: {json.dumps({'type': 'log', 'message': 'In attesa del primo avvio sync...'})}\n\n" - time.sleep(2) - yield from generate() - except GeneratorExit: - pass - - return Response( - stream_with_context(generate()), - mimetype="text/event-stream", - headers={ - "Cache-Control": "no-cache", - "X-Accel-Buffering": "no", - }, - ) - - -@app.route("/api/stream") -def api_stream_default(): - """Compat: stream log della sync di default.""" - return api_stream(get_default_sync_id()) - - -# ============================================================ -# Avvio server -# ============================================================ - -if __name__ == "__main__": - 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, - ) - diff --git a/web/cron_helper.py b/web/cron_helper.py deleted file mode 100644 index d99e9ec..0000000 --- a/web/cron_helper.py +++ /dev/null @@ -1,232 +0,0 @@ -#!/usr/bin/env python3 -# ============================================================ -# cron_helper.py - Parser di espressioni cron minimale -# ============================================================ -# Calcola i secondi mancanti alla prossima esecuzione basata -# su un'espressione cron standard a 5 campi. -# -# Formato: minuto ora giorno_mese mese giorno_settimana -# -# Supporta: -# * = ogni valore -# */N = ogni N unità (step) -# N = valore esatto -# N,M = lista di valori -# N-M = range di valori -# N-M/S = range con step -# -# Uso: -# python3 cron_helper.py "0 3 * * *" → secondi fino alle 3:00 -# python3 cron_helper.py "*/30 * * * *" next → timestamp prossima esecuzione -# python3 cron_helper.py "0 3 * * *" human → descrizione leggibile -# ============================================================ - -import sys -from datetime import datetime, timedelta - - -def parse_cron_field(field, min_val, max_val): - """ - Parsa un singolo campo dell'espressione cron e ritorna - l'insieme dei valori validi. - - Esempi: - "*" → {0,1,2,...,max_val} - "*/15" → {0,15,30,45} (per i minuti) - "1,15" → {1,15} - "1-5" → {1,2,3,4,5} - "1-10/2" → {1,3,5,7,9} - """ - values = set() - - for part in field.split(","): - if "/" in part: - # Gestione step: */N o range/N - range_part, step = part.split("/", 1) - step = int(step) - - if range_part == "*": - start, end = min_val, max_val - elif "-" in range_part: - start, end = map(int, range_part.split("-", 1)) - else: - start, end = int(range_part), max_val - - values.update(range(start, end + 1, step)) - - elif "-" in part: - # Range: N-M - start, end = map(int, part.split("-", 1)) - values.update(range(start, end + 1)) - - elif part == "*": - # Wildcard: tutti i valori - values.update(range(min_val, max_val + 1)) - - else: - # Valore singolo - values.add(int(part)) - - return values - - -def parse_cron_expression(expression): - """ - Parsa un'espressione cron a 5 campi e ritorna i set di valori validi. - - Campi: minuto(0-59) ora(0-23) giorno_mese(1-31) mese(1-12) giorno_settimana(0-6, 0=dom) - """ - parts = expression.strip().split() - if len(parts) != 5: - raise ValueError( - f"Espressione cron non valida: '{expression}'. " - f"Servono 5 campi: minuto ora giorno_mese mese giorno_settimana" - ) - - minutes = parse_cron_field(parts[0], 0, 59) - hours = parse_cron_field(parts[1], 0, 23) - days = parse_cron_field(parts[2], 1, 31) - months = parse_cron_field(parts[3], 1, 12) - weekdays = parse_cron_field(parts[4], 0, 6) - - return minutes, hours, days, months, weekdays - - -def next_cron_time(expression, now=None): - """ - Calcola il prossimo istante che corrisponde all'espressione cron. - Ritorna un oggetto datetime. - - Cerca fino a 366 giorni nel futuro (copre un anno intero + margine). - """ - if now is None: - now = datetime.now() - - minutes, hours, days, months, weekdays = parse_cron_expression(expression) - - # Parti dal minuto successivo (non eseguire "adesso") - candidate = now.replace(second=0, microsecond=0) + timedelta(minutes=1) - - # Cerca nei prossimi 366 giorni (massimo ~527040 minuti) - max_iterations = 366 * 24 * 60 - for _ in range(max_iterations): - if (candidate.month in months and - candidate.day in days and - candidate.weekday() in _convert_weekdays(weekdays) and - candidate.hour in hours and - candidate.minute in minutes): - return candidate - candidate += timedelta(minutes=1) - - raise ValueError(f"Nessuna esecuzione trovata per '{expression}' nei prossimi 366 giorni") - - -def _convert_weekdays(cron_weekdays): - """ - Converte i giorni della settimana dal formato cron (0=domenica) - al formato Python (0=lunedì). - - Cron: 0=dom, 1=lun, 2=mar, 3=mer, 4=gio, 5=ven, 6=sab - Python: 0=lun, 1=mar, 2=mer, 3=gio, 4=ven, 5=sab, 6=dom - """ - mapping = {0: 6, 1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5} - return {mapping[d] for d in cron_weekdays} - - -def seconds_until_next(expression): - """Calcola i secondi mancanti alla prossima esecuzione cron.""" - now = datetime.now() - next_time = next_cron_time(expression, now) - delta = (next_time - now).total_seconds() - return max(1, int(delta)) - - -def human_readable(expression): - """ - Genera una descrizione leggibile in italiano dell'espressione cron. - Copre i casi più comuni; per espressioni complesse ritorna il formato raw. - """ - parts = expression.strip().split() - if len(parts) != 5: - return expression - - m, h, dom, mon, dow = parts - - # Casi comuni - if m == "*" and h == "*" and dom == "*" and mon == "*" and dow == "*": - return "Ogni minuto" - - if m.startswith("*/") and h == "*" and dom == "*" and mon == "*" and dow == "*": - mins = m.split("/")[1] - return f"Ogni {mins} minuti" - - if h.startswith("*/") and dom == "*" and mon == "*" and dow == "*": - hrs = h.split("/")[1] - if m == "0": - return f"Ogni {hrs} ore" - return f"Al minuto {m}, ogni {hrs} ore" - - if dom == "*" and mon == "*" and dow == "*": - if m.isdigit() and h.isdigit(): - return f"Ogni giorno alle {h.zfill(2)}:{m.zfill(2)}" - if m.isdigit() and "," in h: - ore = h.replace(",", ", ") - return f"Alle ore {ore}, al minuto {m}" - - if dom == "*" and mon == "*" and dow != "*": - giorni_map = {"0": "dom", "1": "lun", "2": "mar", "3": "mer", - "4": "gio", "5": "ven", "6": "sab"} - if "," in dow: - giorni = ", ".join(giorni_map.get(d.strip(), d) for d in dow.split(",")) - elif dow in giorni_map: - giorni = giorni_map[dow] - else: - giorni = dow - if m.isdigit() and h.isdigit(): - return f"Ogni {giorni} alle {h.zfill(2)}:{m.zfill(2)}" - - if dom != "*" and mon == "*" and dow == "*": - if m.isdigit() and h.isdigit(): - return f"Il giorno {dom} di ogni mese alle {h.zfill(2)}:{m.zfill(2)}" - - return f"Cron: {expression}" - - -# ============================================================ -# CLI: usato da sync.sh per calcolare i tempi -# ============================================================ - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Uso: cron_helper.py [seconds|next|human|validate]", file=sys.stderr) - sys.exit(1) - - expr = sys.argv[1] - mode = sys.argv[2] if len(sys.argv) > 2 else "seconds" - - try: - if mode == "seconds": - # Ritorna i secondi alla prossima esecuzione - print(seconds_until_next(expr)) - - elif mode == "next": - # Ritorna il timestamp della prossima esecuzione - next_time = next_cron_time(expr) - print(next_time.strftime("%Y-%m-%d %H:%M:%S")) - - elif mode == "human": - # Ritorna una descrizione leggibile - print(human_readable(expr)) - - elif mode == "validate": - # Valida l'espressione (exit 0 se OK, exit 1 se errore) - parse_cron_expression(expr) - print("OK") - - else: - print(f"Modalità sconosciuta: {mode}", file=sys.stderr) - sys.exit(1) - - except ValueError as e: - print(f"ERRORE: {e}", file=sys.stderr) - sys.exit(1) diff --git a/web/templates/index.html b/web/templates/index.html deleted file mode 100644 index 7d7e5ca..0000000 --- a/web/templates/index.html +++ /dev/null @@ -1,926 +0,0 @@ - - - - - - - S3 Sync Dashboard - - - - - -
-
- - - - Avvio... - -
-
- - Connesso -
-
- -
- - -
- -
-
- Ultima Sincronizzazione -
-
-
--:--
-
In attesa...
-
- - -
-
- Prossima Sincronizzazione -
-
-
--:--
-
-
- - -
-
- File Locali -
📁
-
-
--
-
Calcolo...
-
- - -
-
- Sync Completate -
-
-
0
-
0 errori
-
-
- - -
-

⚙ Configurazione Attiva

-
-
- Endpoint - {{ config.endpoint }} -
-
- Bucket - {{ config.bucket }} -
-
- Prefisso - {{ config.prefix }} -
-
- Modalità - {{ config.sync_mode }} -
-
- Pianificazione - {{ config.schedule_display }}{% if config.schedule_mode == 'cron' %} ({{ config.sync_schedule }}){% endif %} -
-
- Sync all'avvio - {{ config.sync_on_start }} -
-
- Trasferimenti - {{ config.transfers }} paralleli -
-
- Banda - {{ config.bandwidth }} -
-
-
- - -
- -
-
-

📜 Log in Tempo Reale

-
- - -
-
-
-
-
📄
- In attesa dei log... -
-
-
- - -
-
-

🕒 Modifiche Recenti

- 0 modifiche -
-
-
-
📁
- Nessuna modifica registrata -
-
-
-
- - -
-

📊 Storico Sincronizzazioni

-
-
-
📋
- Nessuna sincronizzazione completata -
-
-
- -
- - - -