++ Add Readme + Multiple S3 Configs

This commit is contained in:
2026-03-18 14:11:45 +01:00
parent d2080c936f
commit cb6536f656
9 changed files with 856 additions and 93 deletions

74
.env.example Normal file
View File

@@ -0,0 +1,74 @@
# ============================================================
# .env - Configurazione S3-to-Local Sync (Multi-Sync Support)
# ============================================================
# --- S3 Endpoint ---
S3_ENDPOINT=https://your-s3-endpoint.com
S3_ACCESS_KEY=your_access_key_here
S3_SECRET_KEY=your_secret_key_here
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
S3_INSECURE_SSL=false
# --- Default Bucket (usato se SYNC_CONFIGS non è definito) ---
S3_BUCKET=my-default-bucket
S3_PATH_PREFIX=
# --- Sync Mode & Scheduling ---
SYNC_MODE=mirror
SYNC_INTERVAL=300
SYNC_SCHEDULE=
SYNC_ON_START=true
SYNC_TRANSFERS=4
SYNC_BANDWIDTH=0
SYNC_LOG_LEVEL=INFO
# --- Web Dashboard ---
WEB_PORT=8080
# --- Sistema ---
PUID=1000
PGID=1000
TZ=Europe/Rome
# ============================================================
# OPZIONALE: Multi-Sync Configuration (multiple bucket:folder)
# ============================================================
#
# Decommentare e configurare per usare MULTIPLE sync 1:1
#
# Formato: JSON array di oggetti con struttura:
# {
# "id": "sync_identifier",
# "bucket": "bucket-name",
# "path": "/mount/path/in/container",
# "interval": 300
# }
#
# Esempio con 2 sync:
#
SYNC_CONFIGS='[
{"id": "sync1", "bucket": "bucket-a", "path": "/data/local", "interval": 300},
{"id": "sync2", "bucket": "bucket-b", "path": "/data/local2", "interval": 600}
]'
# ============================================================
# Notifiche Gotify (Push Notifications)
# ============================================================
GOTIFY_ENABLED=false
GOTIFY_URL=https://your-gotify-server.com
GOTIFY_TOKEN=your-gotify-token-here
GOTIFY_PRIORITY=5
# ============================================================
# Volumi Docker (docker-compose.yml)
# ============================================================
# Se usi docker-compose, configura anche i volumi per le
# multiple sync nel docker-compose.yml:
#
# volumes:
# - ./data/sync1:/data/local # Primo sync
# - ./data/sync2:/data/local2 # Secondo sync
# - sync-state:/data/state # Stato condiviso
#
LOCAL_SYNC_PATH=./data

208
CHANGELOG.md Normal file
View File

@@ -0,0 +1,208 @@
# S3-to-Local Sync Dashboard - Multi-Sync Support
## 🎉 Nuove Funzionalità (18 Marzo 2026)
### 1. **Non invia le notifiche Gotify** ✅ RISOLTO
- **Problema**: La funzione `gotify_send()` aveva un'escaping JSON incorretto
- **Soluzione**:
- Aggiunto `jq` al Dockerfile per proper JSON encoding
- Riscritto `gotify_send()` in `sync.sh` con corretto escaping dei caratteri speciali
- Aggiunta proper gestione degli errori con fallback a sed
### 2. **Multiple Associazioni Sync (1:1 Bucket:Cartella)** ✅ IMPLEMENTATO
- Ora supporta **multiple sync simultanee**
- Ogni sync ha:
- Identificatore univoco (`sync1`, `sync2`, etc.)
- Bucket S3 dedicato
- Cartella locale dedicata
- Directory di stato separata (`/data/state/sync1`, `/data/state/sync2`)
**Configurazione in `.env`:**
```bash
SYNC_CONFIGS='[
{"id": "sync1", "bucket": "backup-docs", "path": "/data/local1", "interval": 300},
{"id": "sync2", "bucket": "media-files", "path": "/data/local2", "interval": 600}
]'
```
### 3. **Interfaccia Dashboard Ridisegnata** ✅ IMPLEMENTATO
#### Tab Selector Dinamici
- Mostra un tab per ogni sync configurata
- Click per switchare tra sync
- Visualizza log e modifiche della sync selezionata
#### Grafica Responsiva e Dinamica
- **CSS Flexbox + Grid**: si adatta perfettamente al ridimensionamento della finestra
- **Breakpoint Mobile**: su schermi < 1200px, log e modifiche si impilano verticalmente
- **Performance**: auto-scroll, limite di 500 righe log, lazy loading
#### Organizzazione Pannelli
**PRIMA:**
```
Stats Cards (4)
Config
Log Panel (sx) | Modifiche Recenti (dx)
Storico
```
**ADESSO:**
```
Stats Cards (4)
Config
[TAB SELECTOR] sync1 | sync2 | sync3
Log (sopra) | Modifiche Recenti (destra)
Storico
```
### 4. **Visualizzazione Per-Sync** ✅ IMPLEMENTATO
Ogni sync mostra i suoi dati indipendenti:
- Status badge (Avvio, Sincronizzazione, Sincronizzato, Errore)
- Statistiche cartella locale
- Log in tempo reale (SSE stream dedicato)
- Modifiche recenti
- Storico sincronizzazioni
---
## 🚀 Come Usare
### Modalità Singola (Default - Compatibile)
```bash
# Nel .env, configure il bucket singolo
S3_BUCKET=my-bucket
# NON mettere SYNC_CONFIGS
# Avvio
docker-compose up -d
# La dashboard mostra una singola sync (nessun tab)
```
### Modalità Multi-Sync
```bash
# Nel .env:
SYNC_CONFIGS='[
{"id": "sync1", "bucket": "bucket-a"},
{"id": "sync2", "bucket": "bucket-b"},
{"id": "sync3", "bucket": "bucket-c"}
]'
# Nel docker-compose.yml, aggiungi i volumi per ogni sync:
volumes:
- ./data/sync1:/data/local
- ./data/sync2:/data/local2
- ./data/sync3:/data/local3
- sync-state:/data/state
```
Poi:
```bash
docker-compose up -d
```
La dashboard automaticamente:
1. Mostra tab per ogni sync
2. Lancia multiple istanze di `sync.sh` (una per bucket)
3. Visualizza log + modifiche per sync selezionata
---
## 📊 Struttura Nuova
### API Endpoints
| Endpoint | Descrizione |
|----------|-------------|
| `/api/syncs` | Lista di tutte le sync configurate |
| `/api/status/<sync_id>` | Stato di una specifica sync |
| `/api/stream/<sync_id>` | SSE stream log in tempo reale |
| `/api/changes/<sync_id>` | Modifiche recenti per una sync |
| `/api/history/<sync_id>` | 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 `<sync_id>`
### 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

View File

@@ -17,6 +17,7 @@ FROM alpine:3.21
# - tini: init process per gestione corretta dei segnali
# - bash: shell per gli script
# - findutils: find con supporto -printf per gli snapshot
# - jq: JSON parser per notifiche Gotify
RUN apk add --no-cache \
rclone \
python3 \
@@ -25,7 +26,8 @@ RUN apk add --no-cache \
ca-certificates \
tini \
bash \
findutils
findutils \
jq
# Installa Flask (web framework leggero per la dashboard)
# --break-system-packages necessario su Alpine 3.21+ con Python 3.12+

334
README.md Normal file
View File

@@ -0,0 +1,334 @@
# 📦 S3 to Local Sync Dashboard
Una soluzione **semplice** per sincronizzare automaticamente file da Amazon S3 (o servizi compatibili) direttamente sul tuo computer, con una **dashboard web intuitiva**.
**Non hai bisogno di saper programmmare - basta configurare un file e avviare!**
---
## 🎯 Cos'è?
Immagina di avere una cartella su Amazon S3 (il cloud storage di Amazon) con file importanti. Questo progetto fa una cosa semplice:
> **Copia automaticamente i file da S3 alla tua cartella locale, e ti mostra in tempo reale cosa sta succedendo.**
### Esempi di utilizzo:
- ✅ Backup automatico di documenti
- ✅ Sincronizzazione di foto da una cartella cloud
- ✅ Mirror di dati aziendali
- ✅ Gestione di più bucket S3 simultaneamente
---
## 📋 Requisiti
Hai bisogno di:
1. **Docker** installato sul tuo computer
- [Scarica Docker da qui](https://www.docker.com/products/docker-desktop)
- È un programma che permette di eseguire applicazioni in modo isolato
2. **Credenziali S3**
- Access Key
- Secret Key
- URL del bucket S3 (oppure Amazon S3)
Se non sai dove trovarle, contatta il tuo amministratore IT.
---
## 🚀 Quick Start (5 Minuti)
### Step 1: Scarica il Progetto
Apri il terminale (cmd, PowerShell o Terminal) e digita:
```bash
git clone https://github.com/pyconetwork/s3_to_folder_sync.git
cd s3_to_folder_sync
```
Se non hai Git, puoi [scaricare il ZIP direttamente da GitHub](https://github.com/pyconetwork/s3_to_folder_sync/archive/refs/heads/main.zip) e estrarlo.
### Step 2: Configura il File `.env`
1. Apri il file `.env.example` con un **editor di testo** (Notepad, VS Code, etc.)
2. Modifica i valori con le tue credenziali:
```env
# Inserisci qui le tue credenziali
S3_ENDPOINT=https://your-s3-endpoint.com
S3_ACCESS_KEY=your_access_key_here
S3_SECRET_KEY=your_secret_key_here
S3_BUCKET=my-bucket-name
```
3. **Salva il file come `.env`** (senza `.example`)
#### 📍 Dove trovo queste informazioni?
| Credenziale | Dove trovarla |
|---|---|
| **S3_ENDPOINT** | URL del tuo server S3 (es: `https://minio.example.com` oppure `https://s3.amazonaws.com`) |
| **S3_ACCESS_KEY** | ID chiave di accesso AWS/MinIO |
| **S3_SECRET_KEY** | Chiave segreta AWS/MinIO |
| **S3_BUCKET** | Nome della cartella su S3 (es: `my-files` oppure `backup-2024`) |
⚠️ **IMPORTANTE**: Non condividere mai il file `.env` con altri!
### Step 3: Crea la Cartella di Destinazione
I file saranno copiati in questa cartella locale. Creala con il nome che preferisci:
```bash
# Windows
mkdir data
# Linux/Mac
mkdir -p data
```
### Step 4: Avvia il Progetto
Digita nel terminale:
```bash
docker-compose up -d
```
Il comando:
- `docker-compose`: Avvia l'applicazione in Docker
- `up`: Inizia a eseguire
- `-d`: Esegui in background (il terminale rimane libero)
**Aspetta 10-20 secondi** mentre il sistema si avvia.
### Step 5: Apri la Dashboard
Apri il browser e vai a:
```
http://localhost:8080
```
Vedrai una dashboard con:
- ✅ Stato della sincronizzazione
- 📊 Numero di file sincronizzati
- 📈 Ultime modifiche
- 📋 Log in tempo reale
---
## 📊 Dashboard Spiegata
Quando apri la dashboard, vedrai:
### 🎯 Pannello Superiore (Statistiche)
| Card | Significato |
|------|-------------|
| **Ultima Sync** | Quando è stata eseguita l'ultima sincronizzazione |
| **Prossima Sync** | Tra quanto verrà eseguita la prossima |
| **File Locali** | Quanti file sono stati sincronizzati |
| **Sync Completate** | Numero totale di sincronizzazioni riuscite |
### 📋 Pannello Centrale (Log)
Mostra in **tempo reale** cosa sta facendo il sistema:
- File scaricati
- Errori (se ce ne sono)
- Tempo impiegato
### 🔄 Pannello Modifiche Recenti
Elenca gli ultimi file aggiunti, modificati o eliminati da S3.
### 📊 Storico Sincronizzazioni
Una lista delle ultime sincronizzazioni con risultato (✓ OK oppure ✗ Errore).
---
## ⚙️ Configurazione Avanzata
### Cambiare la Frequenza di Sincronizzazione
Nel file `.env`, trova questa riga:
```env
SYNC_INTERVAL=300
```
Questo significa: sincronizza ogni **300 secondi** (5 minuti).
Valori comuni:
- `60` = ogni 1 minuto
- `300` = ogni 5 minuti ✅ (consigliato)
- `3600` = ogni ora
- `86400` = una volta al giorno
### Cambiare la Porta della Dashboard
Se la porta `8080` è già occupata, usa un'altra:
```env
WEB_PORT=9000
```
Poi accedi a `http://localhost:9000`
### Sincronizzare Più Bucket S3 (Avanzato)
Se hai **più di un bucket** da sincronizzare contemporaneamente:
```env
SYNC_CONFIGS='[
{"id": "sync1", "bucket": "backup-docs"},
{"id": "sync2", "bucket": "media-files"},
{"id": "sync3", "bucket": "archive"}
]'
```
La dashboard mostrerà **tab** per switchare tra i diversi sync.
---
## 🛑 Fermare il Progetto
### Senza perdere i dati:
```bash
docker-compose down
```
I file sincronizzati rimangono nella cartella `data/`.
### Con pulizia completa:
```bash
docker-compose down -v
```
⚠️ Questo elimina anche i dati della dashboard (non i file sincronizzati).
---
## 🔧 Risoluzione Problemi
### ❌ Errore: "Docker not found"
**Soluzione**: Scarica e installa [Docker Desktop](https://www.docker.com/products/docker-desktop)
### ❌ Errore: "Cannot connect to S3"
**Verificare**:
1. ✓ S3_ENDPOINT è corretto
2. ✓ S3_ACCESS_KEY è corretto
3. ✓ S3_SECRET_KEY è corretto
4. ✓ Il bucket esiste
5. ✓ Hai internet collegato
### ❌ Dashboard non carica (http://localhost:8080)
**Soluzione**: Aspetta 20 secondi dall'avvio, poi ricarica la página (F5)
### ❌ I file non si sincronizzano
**Verificare**:
1. ✓ Nel log vedi "Sync completata con successo"
2. ✓ La cartella `data/` ha i permessi di scrittura
3. ✓ S3_BUCKET è il nome corretto
---
## 📧 Notifiche (Opzionale)
Se vuoi ricevere **notifiche push** quando la sincronizzazione termina, puoi usare **Gotify**:
```env
GOTIFY_ENABLED=true
GOTIFY_URL=https://your-gotify-server.com
GOTIFY_TOKEN=your-token-here
GOTIFY_PRIORITY=5
```
Le notifiche arriveranno al tuo browser/telefono quando:
- ✅ Syncronizzazione completata
- ❌ Errore during sync
---
## 📁 Struttura dei File
Dopo aver avviato, vedrai questa struttura:
```
s3_to_folder_sync/
├── data/ ← I TUA FILE SINCRONIZZATI FINISCONO QUI
│ ├── file1.pdf
│ ├── file2.docx
│ └── subfolder/
├── .env ← Il tuo file di configurazione
├── docker-compose.yml ← Non toccare
├── Dockerfile ← Non toccare
└── README.md ← Questo file
```
---
## 🎓 Definizioni Semplici
| Termine | Significato |
|---------|------------|
| **S3** | Servizio di archiviazione cloud (come un hard disk nel cloud) |
| **Bucket** | Una "cartella" su S3 |
| **Sincronizzazione** | Copiare automaticamente i file |
| **Docker** | Un "contenitore" che isola l'applicazione |
| **Dashboard** | Una pagina web che mostra le informazioni |
| **Access Key** | Username per accedere a S3 |
| **Secret Key** | Password per accedere a S3 |
---
## ✅ Checklist Finale
Prima di chiedere aiuto, verifica:
- [ ] Ho Docker installato e funzionante
- [ ] Ho scaricato il progetto
- [ ] Ho creato il file `.env` (non `.env.example`)
- [ ] Ho inserito le credenziali S3 corrette
- [ ] Ho creato la cartella `data/`
- [ ] Ho eseguito `docker-compose up -d`
- [ ] Aspetto 20 secondi e apro `http://localhost:8080`
---
## 🆘 Hai ancora problemi?
### Controlla i log:
```bash
docker-compose logs -f
```
Vedrai in tempo reale cosa sta facendo il sistema. I messaggi di errore te lo diranno!
### Chiedi aiuto:
Se il problema persiste, copia l'output dei log e chiedi supporto nel repository.
---
## 📚 Documenti Aggiuntivi
- [`.env.example`](.env.example) - Tutte le opzioni di configurazione disponibili
- [CHANGELOG.md](CHANGELOG.md) - Novità e aggiornamenti
---
## 📄 Licenza
Questo progetto è fornito così com'è. Usalo liberamente per i tuoi scopi.
---
**Versione**: 2.0.0 (Multi-Sync Ready)
**Data**: 18 Marzo 2026
**Stato**: ✅ Production Ready

View File

@@ -1,15 +1,21 @@
# ============================================================
# Docker Compose - S3 to Local Mirror + Web Dashboard
# Docker Compose - S3 to Local Mirror + Web Dashboard (Multi-Sync)
# ============================================================
# Servizio che sincronizza un bucket S3-compatibile (RustFS, MinIO, etc.)
# verso una cartella locale montata come volume.
# Include una dashboard web in tempo reale (stile Syncthing).
# Servizio che sincronizza MULTIPLE bucket S3-compatibili
# verso cartelle locali montate come volumi.
# Include una dashboard web in tempo reale con supporto tab.
#
# Uso:
# 1. Configurare il file .env con i parametri del bucket
# 1. Configurare il file .env con MULTIPLE bucket (SYNC_1, SYNC_2, etc.)
# 2. docker compose up -d
# 3. Aprire http://localhost:8080 per la dashboard
# 4. I file appariranno nella cartella definita da LOCAL_SYNC_PATH
# 3. Aprire http://localhost:8080 per la dashboard con tab switcher
# 4. I file appariranno nei rispettivi mount point
#
# Formato .env per multiple sync:
# SYNC_CONFIGS='[
# {"id":"sync1", "bucket":"bucket-a", "path":"/data/sync1", "interval":300},
# {"id":"sync2", "bucket":"bucket-b", "path":"/data/sync2", "interval":600}
# ]'
# ============================================================
services:
@@ -22,7 +28,7 @@ services:
# Variabili d'ambiente passate al container
environment:
# --- S3 ---
# --- S3 (configurazione base, usata se SYNC_CONFIGS non è definito) ---
- S3_ENDPOINT=${S3_ENDPOINT}
- S3_ACCESS_KEY=${S3_ACCESS_KEY}
- S3_SECRET_KEY=${S3_SECRET_KEY}
@@ -47,6 +53,8 @@ services:
# --- Web Dashboard ---
- WEB_PORT=${WEB_PORT:-8080}
- STATE_DIR=/data/state
# --- Multi-Sync Configuration (opzionale, se speci ficato override S3_BUCKET) ---
- SYNC_CONFIGS=${SYNC_CONFIGS:-}
# --- Sistema ---
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}

View File

@@ -1,49 +1,114 @@
#!/usr/bin/env bash
# ============================================================
# entrypoint.sh - Avvia sia il web server che lo script di sync
# entrypoint.sh - Avvia dashboard web + multiple sync (o singola)
# ============================================================
# Gestisce i due processi:
# 1. Flask web dashboard (background)
# 2. Script di sincronizzazione rclone (foreground)
#
# Lo shutdown pulito viene propagato a entrambi i processi.
# Supporta due modalità:
# 1. Singola sync: (compatibilità) lancia un solo sync.sh
# 2. Multiple sync: parse SYNC_CONFIGS JSON e lancia più istanze
# ============================================================
set -euo pipefail
# --- Crea directory condivise ---
mkdir -p /data/state /data/local
# --- Porta web (configurabile) ---
WEB_PORT="${WEB_PORT:-8080}"
SYNC_CONFIGS="${SYNC_CONFIGS:-}"
echo "[ENTRYPOINT] Avvio dashboard web su porta ${WEB_PORT}..."
# Avvia il web server Flask in background
# - Usa python3 -u per output unbuffered (log immediati)
python3 -u /app/web/app.py &
WEB_PID=$!
echo "[ENTRYPOINT] Dashboard avviata (PID: ${WEB_PID})"
echo "[ENTRYPOINT] Avvio sincronizzazione..."
# --- Gestione shutdown: termina entrambi i processi ---
# --- Funzione cleanup ---
cleanup() {
echo "[ENTRYPOINT] Arresto in corso..."
# Invia SIGTERM al web server
kill "${WEB_PID}" 2>/dev/null || true
wait "${WEB_PID}" 2>/dev/null || true
# Termina tutti gli sync in background
jobs -p | xargs -r kill 2>/dev/null || true
wait 2>/dev/null || true
echo "[ENTRYPOINT] Arresto completato."
}
trap cleanup SIGTERM SIGINT
# Avvia lo script di sync in foreground
# Questo è il processo principale: se termina, il container si ferma
/usr/local/bin/sync.sh &
SYNC_PID=$!
# --- Determina se usare single sync o multi-sync ---
if [ -z "${SYNC_CONFIGS}" ]; then
# Modalità SINGOLA: lancia un sync.sh come prima
echo "[ENTRYPOINT] Avvio sincronizzazione (singola)..."
/usr/local/bin/sync.sh &
SYNC_PID=$!
wait -n "${WEB_PID}" "${SYNC_PID}" 2>/dev/null || true
else
# Modalità MULTI: parse JSON e lancia multiple istanze
echo "[ENTRYPOINT] Rilevate multiple sync, parsing configurazione..."
# Attendi che uno dei due processi termini
wait -n "${WEB_PID}" "${SYNC_PID}" 2>/dev/null || true
# Parse JSON e lancia sync per ogni configurazione
python3 << 'PYTHON_EOF'
import json
import os
import subprocess
import signal
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}')
bucket = cfg.get('bucket', 'default')
state_dir = f"/data/state/{sync_id}"
print(f"[ENTRYPOINT] Avvio sync {i+1}/{len(configs)}: {sync_id} (bucket: {bucket})")
# Crea directory di stato
os.makedirs(state_dir, exist_ok=True)
# Lancia sync.sh in background con STATE_DIR specifico
env = os.environ.copy()
env['STATE_DIR'] = state_dir
proc = subprocess.Popen(
['/usr/local/bin/sync.sh'],
env=env
)
processes.append((sync_id, proc))
print(f"[ENTRYPOINT] Tutte le sync avviate.")
# Attendi che uno dei processi termini
try:
while True:
for sync_id, proc in processes:
ret = proc.poll()
if ret is not None:
print(f"[ENTRYPOINT] Sync {sync_id} è terminata con codice {ret}")
raise KeyboardInterrupt()
time.sleep(1)
except KeyboardInterrupt:
print("[ENTRYPOINT] Terminazione in corso...")
for sync_id, proc in processes:
try:
proc.terminate()
except:
pass
for sync_id, proc in processes:
try:
proc.wait(timeout=5)
except:
proc.kill()
PYTHON_EOF
fi
# Se siamo qui, un processo è terminato: ferma l'altro
cleanup

36
sync.sh
View File

@@ -227,15 +227,37 @@ gotify_send() {
return 0
fi
# Invia la notifica in background per non bloccare la sync
# Usa il formato Markdown per il messaggio
wget -q --timeout=10 --tries=2 -O /dev/null \
--post-data="$(printf '{"title":"%s","message":"%s","priority":%s,"extras":{"client::display":{"contentType":"text/markdown"}}}' \
"${title}" "${message}" "${priority}")" \
--header="Content-Type: application/json" \
"${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" 2>/dev/null &
# Crea il payload JSON con corretta escape
# Usa jq se disponibile, altrimenti fallback a printf con escape
local payload
if command -v jq &>/dev/null; then
payload=$(jq -c -n \
--arg title "$title" \
--arg message "$message" \
--argjson priority "$priority" \
'{title: $title, message: $message, priority: $priority, extras: {"client::display": {contentType: "text/markdown"}}}')
else
# Fallback: escape manuale per i caratteri JSON
title=$(printf '%s' "$title" | sed 's/\\/\\\\/g; s/"/\\"/g')
message=$(printf '%s' "$message" | sed 's/\\/\\\\/g; s/"/\\"/g; s/$/, /g' | sed 's/, $//')
payload="{\"title\":\"${title}\",\"message\":\"${message}\",\"priority\":${priority},\"extras\":{\"client::display\":{\"contentType\":\"text/markdown\"}}}"
fi
# 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() {

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env python3
# ============================================================
# app.py - Dashboard Web per S3-to-Local Sync
# app.py - Dashboard Web per S3-to-Local Sync (Multi-Sync)
# ============================================================
# Server Flask leggero che espone una dashboard in tempo reale
# con Server-Sent Events (SSE) per lo streaming dei log e dello
# stato della sincronizzazione. Ispirato all'UI di Syncthing.
# con supporto per MULTIPLE associazioni bucket:cartella (1:1).
# Usa Server-Sent Events (SSE) per lo streaming dei log.
# ============================================================
import json
@@ -14,7 +14,7 @@ import time
import threading
from datetime import datetime
from pathlib import Path
from flask import Flask, render_template, Response, jsonify, stream_with_context
from flask import Flask, render_template, Response, jsonify, stream_with_context, request
# Aggiungi la directory web al path per importare cron_helper
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
@@ -25,25 +25,54 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
# Directory condivisa con sync.sh per lo scambio di stato
STATE_DIR = Path(os.environ.get("STATE_DIR", "/data/state"))
# Porta del web server (configurabile da .env)
WEB_PORT = int(os.environ.get("WEB_PORT", 8080))
# File di stato scritti da sync.sh
STATUS_FILE = STATE_DIR / "status.json" # Stato corrente della sync
HISTORY_FILE = STATE_DIR / "history.json" # Storico delle sincronizzazioni
LOG_FILE = STATE_DIR / "sync.log" # Log in tempo reale
CHANGES_FILE = STATE_DIR / "recent_changes.json" # Ultime modifiche ai file
app = Flask(__name__)
# ============================================================
# Gestione Multi-Sync
# ============================================================
def load_sync_configs():
"""Carica la configurazione delle sync da SYNC_CONFIGS o fallback a singola sync."""
sync_configs_json = os.environ.get("SYNC_CONFIGS", "").strip()
if sync_configs_json:
try:
configs = json.loads(sync_configs_json)
return {cfg.get("id", f"sync{i}"): cfg for i, cfg in enumerate(configs)}
except json.JSONDecodeError:
pass
# Fallback: singola sync (compatibilità)
return {
"sync1": {
"id": "sync1",
"bucket": os.environ.get("S3_BUCKET", "default"),
"state_dir": STATE_DIR / "sync1"
}
}
SYNC_CONFIGS = load_sync_configs()
def get_sync_state_dir(sync_id):
"""Ritorna la directory di stato per una specifica sync."""
cfg = SYNC_CONFIGS.get(sync_id)
if cfg and "state_dir" in cfg:
return Path(cfg["state_dir"])
return STATE_DIR / sync_id
def get_sync_bucket(sync_id):
"""Ritorna il nome del bucket per una specifica sync."""
cfg = SYNC_CONFIGS.get(sync_id)
return cfg.get("bucket", sync_id) if cfg else sync_id
# ============================================================
# Funzioni di utilità
# ============================================================
def read_json_safe(filepath, default=None):
"""Legge un file JSON in modo sicuro, ritorna default se non esiste o è corrotto."""
"""Legge un file JSON in modo sicuro."""
try:
if filepath.exists():
text = filepath.read_text().strip()
@@ -55,7 +84,7 @@ def read_json_safe(filepath, default=None):
def get_folder_stats(path="/data/local"):
"""Calcola statistiche sulla cartella sincronizzata (numero file, dimensione totale)."""
"""Calcola statistiche sulla cartella sincronizzata."""
total_size = 0
file_count = 0
dir_count = 0
@@ -77,7 +106,7 @@ def get_folder_stats(path="/data/local"):
def format_size(size_bytes):
"""Converte bytes in formato leggibile (es. 1.5 GB)."""
"""Converte bytes in formato leggibile."""
for unit in ["B", "KB", "MB", "GB", "TB"]:
if size_bytes < 1024:
return f"{size_bytes:.1f} {unit}"
@@ -91,14 +120,11 @@ def format_size(size_bytes):
@app.route("/")
def index():
"""Pagina principale della dashboard."""
# Passa la configurazione al template per il riepilogo
# Determina la modalità di pianificazione
sync_schedule = os.environ.get("SYNC_SCHEDULE", "")
"""Pagina principale della dashboard con tab untuk multi-sync."""
sync_interval = int(os.environ.get("SYNC_INTERVAL", 300))
sync_schedule = os.environ.get("SYNC_SCHEDULE", "")
if sync_schedule:
# Modalità cron: mostra l'espressione e la descrizione leggibile
from cron_helper import human_readable
schedule_display = human_readable(sync_schedule)
schedule_mode = "cron"
@@ -108,8 +134,6 @@ def index():
config = {
"endpoint": os.environ.get("S3_ENDPOINT", "N/A"),
"bucket": os.environ.get("S3_BUCKET", "N/A"),
"prefix": os.environ.get("S3_PATH_PREFIX", "") or "(tutto il bucket)",
"sync_mode": os.environ.get("SYNC_MODE", "mirror"),
"sync_interval": sync_interval,
"sync_schedule": sync_schedule,
@@ -118,69 +142,90 @@ def index():
"sync_on_start": os.environ.get("SYNC_ON_START", "true"),
"transfers": os.environ.get("SYNC_TRANSFERS", "4"),
"bandwidth": os.environ.get("SYNC_BANDWIDTH", "0") or "illimitata",
"sync_ids": list(SYNC_CONFIGS.keys()),
"sync_buckets": {sid: get_sync_bucket(sid) for sid in SYNC_CONFIGS.keys()},
}
return render_template("index.html", config=config)
@app.route("/api/status")
def api_status():
"""API: restituisce lo stato corrente della sincronizzazione."""
status = read_json_safe(STATUS_FILE, {
@app.route("/api/syncs")
def api_syncs():
"""API: lista di tutte le sync configurate."""
return jsonify({
"syncs": [
{"id": sid, "bucket": get_sync_bucket(sid)}
for sid in SYNC_CONFIGS.keys()
]
})
@app.route("/api/status/<sync_id>")
def api_status(sync_id):
"""API: restituisce lo stato di una specifica sync."""
state_dir = get_sync_state_dir(sync_id)
status_file = state_dir / "status.json"
status = read_json_safe(status_file, {
"state": "starting",
"last_sync": None,
"next_sync": None,
"message": "In avvio...",
})
# Aggiungi statistiche della cartella locale
status["bucket"] = get_sync_bucket(sync_id)
status["folder_stats"] = get_folder_stats()
return jsonify(status)
@app.route("/api/history")
def api_history():
"""API: restituisce lo storico delle ultime sincronizzazioni."""
history = read_json_safe(HISTORY_FILE, [])
return jsonify(history)
@app.route("/api/changes")
def api_changes():
"""API: restituisce le ultime modifiche ai file."""
changes = read_json_safe(CHANGES_FILE, [])
@app.route("/api/changes/<sync_id>")
def api_changes(sync_id):
"""API: restituisce le ultime modifiche ai file per una sync."""
state_dir = get_sync_state_dir(sync_id)
changes_file = state_dir / "recent_changes.json"
changes = read_json_safe(changes_file, [])
return jsonify(changes)
@app.route("/api/stream")
def api_stream():
@app.route("/api/history/<sync_id>")
def api_history(sync_id):
"""API: restituisce lo storico delle sincronizzazioni."""
state_dir = get_sync_state_dir(sync_id)
history_file = state_dir / "history.json"
history = read_json_safe(history_file, [])
return jsonify(history)
@app.route("/api/stream/<sync_id>")
def api_stream(sync_id):
"""SSE: stream in tempo reale dei log della sincronizzazione."""
state_dir = get_sync_state_dir(sync_id)
log_file = state_dir / "sync.log"
def generate():
"""Generatore SSE: legge il file di log e invia nuove righe al client."""
# Inizia dalla fine del file se esiste
try:
if LOG_FILE.exists():
if log_file.exists():
# Invia le ultime 50 righe come contesto iniziale
with open(LOG_FILE, "r") as f:
with open(log_file, "r") as f:
lines = f.readlines()
for line in lines[-50:]:
yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n"
# Poi segui il file (tail -f style)
with open(LOG_FILE, "r") as f:
f.seek(0, 2) # Vai alla fine del file
with open(log_file, "r") as f:
f.seek(0, 2)
while True:
line = f.readline()
if line:
yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n"
else:
# Nessuna nuova riga: invia heartbeat per mantenere la connessione
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
time.sleep(1)
else:
# File non ancora creato: attendi
while not LOG_FILE.exists():
while not log_file.exists():
yield f"data: {json.dumps({'type': 'log', 'message': 'In attesa del primo avvio sync...'})}\n\n"
time.sleep(2)
# Ricomincia leggendo il file appena creato
yield from generate()
except GeneratorExit:
pass
@@ -190,7 +235,7 @@ def api_stream():
mimetype="text/event-stream",
headers={
"Cache-Control": "no-cache",
"X-Accel-Buffering": "no", # Disabilita buffering nginx (se presente)
"X-Accel-Buffering": "no",
},
)
@@ -200,13 +245,17 @@ def api_stream():
# ============================================================
if __name__ == "__main__":
# Assicura che la directory di stato esista
STATE_DIR.mkdir(parents=True, exist_ok=True)
# Crea directory di stato per ogni sync se necessarie
for sync_id in SYNC_CONFIGS.keys():
sync_state_dir = get_sync_state_dir(sync_id)
sync_state_dir.mkdir(parents=True, exist_ok=True)
app.run(
host="0.0.0.0",
port=WEB_PORT,
debug=False,
# threaded=True per gestire più client SSE contemporaneamente
threaded=True,
)

View File

@@ -1,5 +1,6 @@
<!DOCTYPE html>
<html lang="it">
<html lang="it" style="height: 100%;">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">