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 -
-
-
- -
- - - -