++ Add Readme + Multiple S3 Configs
This commit is contained in:
74
.env.example
Normal file
74
.env.example
Normal 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
208
CHANGELOG.md
Normal 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
|
||||||
@@ -17,6 +17,7 @@ FROM alpine:3.21
|
|||||||
# - tini: init process per gestione corretta dei segnali
|
# - tini: init process per gestione corretta dei segnali
|
||||||
# - bash: shell per gli script
|
# - bash: shell per gli script
|
||||||
# - findutils: find con supporto -printf per gli snapshot
|
# - findutils: find con supporto -printf per gli snapshot
|
||||||
|
# - jq: JSON parser per notifiche Gotify
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
rclone \
|
rclone \
|
||||||
python3 \
|
python3 \
|
||||||
@@ -25,7 +26,8 @@ RUN apk add --no-cache \
|
|||||||
ca-certificates \
|
ca-certificates \
|
||||||
tini \
|
tini \
|
||||||
bash \
|
bash \
|
||||||
findutils
|
findutils \
|
||||||
|
jq
|
||||||
|
|
||||||
# Installa Flask (web framework leggero per la dashboard)
|
# Installa Flask (web framework leggero per la dashboard)
|
||||||
# --break-system-packages necessario su Alpine 3.21+ con Python 3.12+
|
# --break-system-packages necessario su Alpine 3.21+ con Python 3.12+
|
||||||
|
|||||||
334
README.md
Normal file
334
README.md
Normal 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
|
||||||
@@ -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.)
|
# Servizio che sincronizza MULTIPLE bucket S3-compatibili
|
||||||
# verso una cartella locale montata come volume.
|
# verso cartelle locali montate come volumi.
|
||||||
# Include una dashboard web in tempo reale (stile Syncthing).
|
# Include una dashboard web in tempo reale con supporto tab.
|
||||||
#
|
#
|
||||||
# Uso:
|
# 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
|
# 2. docker compose up -d
|
||||||
# 3. Aprire http://localhost:8080 per la dashboard
|
# 3. Aprire http://localhost:8080 per la dashboard con tab switcher
|
||||||
# 4. I file appariranno nella cartella definita da LOCAL_SYNC_PATH
|
# 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:
|
services:
|
||||||
@@ -22,7 +28,7 @@ services:
|
|||||||
|
|
||||||
# Variabili d'ambiente passate al container
|
# Variabili d'ambiente passate al container
|
||||||
environment:
|
environment:
|
||||||
# --- S3 ---
|
# --- S3 (configurazione base, usata se SYNC_CONFIGS non è definito) ---
|
||||||
- S3_ENDPOINT=${S3_ENDPOINT}
|
- S3_ENDPOINT=${S3_ENDPOINT}
|
||||||
- S3_ACCESS_KEY=${S3_ACCESS_KEY}
|
- S3_ACCESS_KEY=${S3_ACCESS_KEY}
|
||||||
- S3_SECRET_KEY=${S3_SECRET_KEY}
|
- S3_SECRET_KEY=${S3_SECRET_KEY}
|
||||||
@@ -47,6 +53,8 @@ services:
|
|||||||
# --- Web Dashboard ---
|
# --- Web Dashboard ---
|
||||||
- WEB_PORT=${WEB_PORT:-8080}
|
- WEB_PORT=${WEB_PORT:-8080}
|
||||||
- STATE_DIR=/data/state
|
- STATE_DIR=/data/state
|
||||||
|
# --- Multi-Sync Configuration (opzionale, se speci ficato override S3_BUCKET) ---
|
||||||
|
- SYNC_CONFIGS=${SYNC_CONFIGS:-}
|
||||||
# --- Sistema ---
|
# --- Sistema ---
|
||||||
- PUID=${PUID:-1000}
|
- PUID=${PUID:-1000}
|
||||||
- PGID=${PGID:-1000}
|
- PGID=${PGID:-1000}
|
||||||
|
|||||||
107
entrypoint.sh
107
entrypoint.sh
@@ -1,49 +1,114 @@
|
|||||||
#!/usr/bin/env bash
|
#!/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:
|
# Supporta due modalità:
|
||||||
# 1. Flask web dashboard (background)
|
# 1. Singola sync: (compatibilità) lancia un solo sync.sh
|
||||||
# 2. Script di sincronizzazione rclone (foreground)
|
# 2. Multiple sync: parse SYNC_CONFIGS JSON e lancia più istanze
|
||||||
#
|
|
||||||
# Lo shutdown pulito viene propagato a entrambi i processi.
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# --- Crea directory condivise ---
|
|
||||||
mkdir -p /data/state /data/local
|
mkdir -p /data/state /data/local
|
||||||
|
|
||||||
# --- Porta web (configurabile) ---
|
|
||||||
WEB_PORT="${WEB_PORT:-8080}"
|
WEB_PORT="${WEB_PORT:-8080}"
|
||||||
|
SYNC_CONFIGS="${SYNC_CONFIGS:-}"
|
||||||
|
|
||||||
echo "[ENTRYPOINT] Avvio dashboard web su porta ${WEB_PORT}..."
|
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 &
|
python3 -u /app/web/app.py &
|
||||||
WEB_PID=$!
|
WEB_PID=$!
|
||||||
|
|
||||||
echo "[ENTRYPOINT] Dashboard avviata (PID: ${WEB_PID})"
|
echo "[ENTRYPOINT] Dashboard avviata (PID: ${WEB_PID})"
|
||||||
echo "[ENTRYPOINT] Avvio sincronizzazione..."
|
|
||||||
|
|
||||||
# --- Gestione shutdown: termina entrambi i processi ---
|
# --- Funzione cleanup ---
|
||||||
cleanup() {
|
cleanup() {
|
||||||
echo "[ENTRYPOINT] Arresto in corso..."
|
echo "[ENTRYPOINT] Arresto in corso..."
|
||||||
# Invia SIGTERM al web server
|
|
||||||
kill "${WEB_PID}" 2>/dev/null || true
|
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."
|
echo "[ENTRYPOINT] Arresto completato."
|
||||||
}
|
}
|
||||||
trap cleanup SIGTERM SIGINT
|
trap cleanup SIGTERM SIGINT
|
||||||
|
|
||||||
# Avvia lo script di sync in foreground
|
# --- Determina se usare single sync o multi-sync ---
|
||||||
# Questo è il processo principale: se termina, il container si ferma
|
if [ -z "${SYNC_CONFIGS}" ]; then
|
||||||
|
# Modalità SINGOLA: lancia un sync.sh come prima
|
||||||
|
echo "[ENTRYPOINT] Avvio sincronizzazione (singola)..."
|
||||||
/usr/local/bin/sync.sh &
|
/usr/local/bin/sync.sh &
|
||||||
SYNC_PID=$!
|
SYNC_PID=$!
|
||||||
|
|
||||||
# Attendi che uno dei due processi termini
|
|
||||||
wait -n "${WEB_PID}" "${SYNC_PID}" 2>/dev/null || true
|
wait -n "${WEB_PID}" "${SYNC_PID}" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
# Modalità MULTI: parse JSON e lancia multiple istanze
|
||||||
|
echo "[ENTRYPOINT] Rilevate multiple sync, parsing configurazione..."
|
||||||
|
|
||||||
|
# Parse JSON e lancia sync per ogni configurazione
|
||||||
|
python3 << 'PYTHON_EOF'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import signal
|
||||||
|
import time
|
||||||
|
|
||||||
|
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
|
cleanup
|
||||||
|
|
||||||
|
|||||||
36
sync.sh
36
sync.sh
@@ -227,15 +227,37 @@ gotify_send() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Invia la notifica in background per non bloccare la sync
|
# Crea il payload JSON con corretta escape
|
||||||
# Usa il formato Markdown per il messaggio
|
# Usa jq se disponibile, altrimenti fallback a printf con escape
|
||||||
wget -q --timeout=10 --tries=2 -O /dev/null \
|
local payload
|
||||||
--post-data="$(printf '{"title":"%s","message":"%s","priority":%s,"extras":{"client::display":{"contentType":"text/markdown"}}}' \
|
if command -v jq &>/dev/null; then
|
||||||
"${title}" "${message}" "${priority}")" \
|
payload=$(jq -c -n \
|
||||||
--header="Content-Type: application/json" \
|
--arg title "$title" \
|
||||||
"${GOTIFY_URL}/message?token=${GOTIFY_TOKEN}" 2>/dev/null &
|
--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}"
|
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() {
|
gotify_notify_sync_result() {
|
||||||
|
|||||||
151
web/app.py
151
web/app.py
@@ -1,10 +1,10 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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
|
# Server Flask leggero che espone una dashboard in tempo reale
|
||||||
# con Server-Sent Events (SSE) per lo streaming dei log e dello
|
# con supporto per MULTIPLE associazioni bucket:cartella (1:1).
|
||||||
# stato della sincronizzazione. Ispirato all'UI di Syncthing.
|
# Usa Server-Sent Events (SSE) per lo streaming dei log.
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -14,7 +14,7 @@ import time
|
|||||||
import threading
|
import threading
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
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
|
# Aggiungi la directory web al path per importare cron_helper
|
||||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
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
|
# Directory condivisa con sync.sh per lo scambio di stato
|
||||||
STATE_DIR = Path(os.environ.get("STATE_DIR", "/data/state"))
|
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))
|
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__)
|
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à
|
# Funzioni di utilità
|
||||||
# ============================================================
|
# ============================================================
|
||||||
|
|
||||||
def read_json_safe(filepath, default=None):
|
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:
|
try:
|
||||||
if filepath.exists():
|
if filepath.exists():
|
||||||
text = filepath.read_text().strip()
|
text = filepath.read_text().strip()
|
||||||
@@ -55,7 +84,7 @@ def read_json_safe(filepath, default=None):
|
|||||||
|
|
||||||
|
|
||||||
def get_folder_stats(path="/data/local"):
|
def get_folder_stats(path="/data/local"):
|
||||||
"""Calcola statistiche sulla cartella sincronizzata (numero file, dimensione totale)."""
|
"""Calcola statistiche sulla cartella sincronizzata."""
|
||||||
total_size = 0
|
total_size = 0
|
||||||
file_count = 0
|
file_count = 0
|
||||||
dir_count = 0
|
dir_count = 0
|
||||||
@@ -77,7 +106,7 @@ def get_folder_stats(path="/data/local"):
|
|||||||
|
|
||||||
|
|
||||||
def format_size(size_bytes):
|
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"]:
|
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
||||||
if size_bytes < 1024:
|
if size_bytes < 1024:
|
||||||
return f"{size_bytes:.1f} {unit}"
|
return f"{size_bytes:.1f} {unit}"
|
||||||
@@ -91,14 +120,11 @@ def format_size(size_bytes):
|
|||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
def index():
|
def index():
|
||||||
"""Pagina principale della dashboard."""
|
"""Pagina principale della dashboard con tab untuk multi-sync."""
|
||||||
# Passa la configurazione al template per il riepilogo
|
|
||||||
# Determina la modalità di pianificazione
|
|
||||||
sync_schedule = os.environ.get("SYNC_SCHEDULE", "")
|
|
||||||
sync_interval = int(os.environ.get("SYNC_INTERVAL", 300))
|
sync_interval = int(os.environ.get("SYNC_INTERVAL", 300))
|
||||||
|
sync_schedule = os.environ.get("SYNC_SCHEDULE", "")
|
||||||
|
|
||||||
if sync_schedule:
|
if sync_schedule:
|
||||||
# Modalità cron: mostra l'espressione e la descrizione leggibile
|
|
||||||
from cron_helper import human_readable
|
from cron_helper import human_readable
|
||||||
schedule_display = human_readable(sync_schedule)
|
schedule_display = human_readable(sync_schedule)
|
||||||
schedule_mode = "cron"
|
schedule_mode = "cron"
|
||||||
@@ -108,8 +134,6 @@ def index():
|
|||||||
|
|
||||||
config = {
|
config = {
|
||||||
"endpoint": os.environ.get("S3_ENDPOINT", "N/A"),
|
"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_mode": os.environ.get("SYNC_MODE", "mirror"),
|
||||||
"sync_interval": sync_interval,
|
"sync_interval": sync_interval,
|
||||||
"sync_schedule": sync_schedule,
|
"sync_schedule": sync_schedule,
|
||||||
@@ -118,69 +142,90 @@ def index():
|
|||||||
"sync_on_start": os.environ.get("SYNC_ON_START", "true"),
|
"sync_on_start": os.environ.get("SYNC_ON_START", "true"),
|
||||||
"transfers": os.environ.get("SYNC_TRANSFERS", "4"),
|
"transfers": os.environ.get("SYNC_TRANSFERS", "4"),
|
||||||
"bandwidth": os.environ.get("SYNC_BANDWIDTH", "0") or "illimitata",
|
"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)
|
return render_template("index.html", config=config)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/status")
|
@app.route("/api/syncs")
|
||||||
def api_status():
|
def api_syncs():
|
||||||
"""API: restituisce lo stato corrente della sincronizzazione."""
|
"""API: lista di tutte le sync configurate."""
|
||||||
status = read_json_safe(STATUS_FILE, {
|
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",
|
"state": "starting",
|
||||||
"last_sync": None,
|
"last_sync": None,
|
||||||
"next_sync": None,
|
"next_sync": None,
|
||||||
"message": "In avvio...",
|
"message": "In avvio...",
|
||||||
})
|
})
|
||||||
|
|
||||||
# Aggiungi statistiche della cartella locale
|
# Aggiungi statistiche della cartella locale
|
||||||
|
status["bucket"] = get_sync_bucket(sync_id)
|
||||||
status["folder_stats"] = get_folder_stats()
|
status["folder_stats"] = get_folder_stats()
|
||||||
return jsonify(status)
|
return jsonify(status)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/history")
|
@app.route("/api/changes/<sync_id>")
|
||||||
def api_history():
|
def api_changes(sync_id):
|
||||||
"""API: restituisce lo storico delle ultime sincronizzazioni."""
|
"""API: restituisce le ultime modifiche ai file per una sync."""
|
||||||
history = read_json_safe(HISTORY_FILE, [])
|
state_dir = get_sync_state_dir(sync_id)
|
||||||
return jsonify(history)
|
changes_file = state_dir / "recent_changes.json"
|
||||||
|
changes = read_json_safe(changes_file, [])
|
||||||
|
|
||||||
@app.route("/api/changes")
|
|
||||||
def api_changes():
|
|
||||||
"""API: restituisce le ultime modifiche ai file."""
|
|
||||||
changes = read_json_safe(CHANGES_FILE, [])
|
|
||||||
return jsonify(changes)
|
return jsonify(changes)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/stream")
|
@app.route("/api/history/<sync_id>")
|
||||||
def api_stream():
|
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."""
|
"""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():
|
def generate():
|
||||||
"""Generatore SSE: legge il file di log e invia nuove righe al client."""
|
"""Generatore SSE: legge il file di log e invia nuove righe al client."""
|
||||||
# Inizia dalla fine del file se esiste
|
|
||||||
try:
|
try:
|
||||||
if LOG_FILE.exists():
|
if log_file.exists():
|
||||||
# Invia le ultime 50 righe come contesto iniziale
|
# 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()
|
lines = f.readlines()
|
||||||
for line in lines[-50:]:
|
for line in lines[-50:]:
|
||||||
yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n"
|
yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n"
|
||||||
|
|
||||||
# Poi segui il file (tail -f style)
|
# Poi segui il file (tail -f style)
|
||||||
with open(LOG_FILE, "r") as f:
|
with open(log_file, "r") as f:
|
||||||
f.seek(0, 2) # Vai alla fine del file
|
f.seek(0, 2)
|
||||||
while True:
|
while True:
|
||||||
line = f.readline()
|
line = f.readline()
|
||||||
if line:
|
if line:
|
||||||
yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n"
|
yield f"data: {json.dumps({'type': 'log', 'message': line.strip()})}\n\n"
|
||||||
else:
|
else:
|
||||||
# Nessuna nuova riga: invia heartbeat per mantenere la connessione
|
|
||||||
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
|
yield f"data: {json.dumps({'type': 'heartbeat'})}\n\n"
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
else:
|
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"
|
yield f"data: {json.dumps({'type': 'log', 'message': 'In attesa del primo avvio sync...'})}\n\n"
|
||||||
time.sleep(2)
|
time.sleep(2)
|
||||||
# Ricomincia leggendo il file appena creato
|
|
||||||
yield from generate()
|
yield from generate()
|
||||||
except GeneratorExit:
|
except GeneratorExit:
|
||||||
pass
|
pass
|
||||||
@@ -190,7 +235,7 @@ def api_stream():
|
|||||||
mimetype="text/event-stream",
|
mimetype="text/event-stream",
|
||||||
headers={
|
headers={
|
||||||
"Cache-Control": "no-cache",
|
"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__":
|
if __name__ == "__main__":
|
||||||
# Assicura che la directory di stato esista
|
|
||||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
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(
|
app.run(
|
||||||
host="0.0.0.0",
|
host="0.0.0.0",
|
||||||
port=WEB_PORT,
|
port=WEB_PORT,
|
||||||
debug=False,
|
debug=False,
|
||||||
# threaded=True per gestire più client SSE contemporaneamente
|
|
||||||
threaded=True,
|
threaded=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="it">
|
<html lang="it" style="height: 100%;">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|||||||
Reference in New Issue
Block a user