627 lines
20 KiB
Bash
627 lines
20 KiB
Bash
#!/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}" <<ENDJSON
|
|
{
|
|
"state": "${state}",
|
|
"message": "${message}",
|
|
"last_sync": ${LAST_SYNC_TS:-null},
|
|
"updated_at": "${now}",
|
|
"sync_count": ${SYNC_COUNT:-0},
|
|
"error_count": ${ERROR_COUNT:-0}
|
|
}
|
|
ENDJSON
|
|
}
|
|
|
|
append_history() {
|
|
# Aggiunge un record allo storico delle sincronizzazioni (max 50 entries)
|
|
local success="$1"
|
|
local duration="$2"
|
|
local transferred="${3:-0}"
|
|
local size="${4:-0 B}"
|
|
local time_str
|
|
time_str="$(date '+%Y-%m-%d %H:%M:%S')"
|
|
|
|
# Leggi storico attuale, aggiungi in testa, limita a 50
|
|
python3 -c "
|
|
import json, sys
|
|
try:
|
|
with open('${HISTORY_FILE}', 'r') as f:
|
|
history = json.load(f)
|
|
except:
|
|
history = []
|
|
entry = {
|
|
'time': '${time_str}',
|
|
'success': ${success},
|
|
'duration': '${duration}',
|
|
'transferred': '${transferred}',
|
|
'size': '${size}'
|
|
}
|
|
history.insert(0, entry)
|
|
history = history[:50]
|
|
with open('${HISTORY_FILE}', 'w') as f:
|
|
json.dump(history, f)
|
|
"
|
|
}
|
|
|
|
update_recent_changes() {
|
|
# Confronta lo stato prima/dopo la sync per trovare le modifiche
|
|
local after_snapshot="$1"
|
|
local before_snapshot="$2"
|
|
|
|
python3 -c "
|
|
import json, os
|
|
from datetime import datetime
|
|
|
|
after_file = '${after_snapshot}'
|
|
before_file = '${before_snapshot}'
|
|
|
|
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]] = {'size': int(parts[0]), 'mtime': parts[1]}
|
|
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_file)
|
|
after = read_snapshot(after_file)
|
|
|
|
changes = []
|
|
now = datetime.now().strftime('%H:%M:%S')
|
|
|
|
# File nuovi
|
|
for path in after:
|
|
if path not in before:
|
|
changes.append({
|
|
'action': 'new',
|
|
'path': path,
|
|
'size': format_size(after[path]['size']),
|
|
'time': now
|
|
})
|
|
|
|
# File modificati
|
|
for path in after:
|
|
if path in before and (after[path]['size'] != before[path]['size'] or after[path]['mtime'] != before[path]['mtime']):
|
|
changes.append({
|
|
'action': 'modified',
|
|
'path': path,
|
|
'size': format_size(after[path]['size']),
|
|
'time': now
|
|
})
|
|
|
|
# File eliminati (solo in modalità mirror)
|
|
for path in before:
|
|
if path not in after:
|
|
changes.append({
|
|
'action': 'deleted',
|
|
'path': path,
|
|
'size': format_size(before[path]['size']),
|
|
'time': now
|
|
})
|
|
|
|
# Mantieni le ultime 100 modifiche
|
|
try:
|
|
with open('${CHANGES_FILE}', 'r') as f:
|
|
old_changes = json.load(f)
|
|
except:
|
|
old_changes = []
|
|
|
|
all_changes = changes + old_changes
|
|
all_changes = all_changes[:100]
|
|
|
|
with open('${CHANGES_FILE}', 'w') as f:
|
|
json.dump(all_changes, f)
|
|
"
|
|
}
|
|
|
|
take_snapshot() {
|
|
# Scatta una "foto" dello stato dei file locali: size|mtime|path
|
|
local output_file="$1"
|
|
find "${LOCAL_PATH}" -type f -printf '%s|%T@|%P\n' 2>/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_<REMOTE>_<PARAMETRO>
|
|
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!"
|