#!/bin/sh # backup_trilium.sh - Backup local horodate de la base Trilium (etage 1). # Execute en ROOT par le planificateur DSM (le dossier backup/ est en 700 uid 1000). # 1) demande a Trilium une copie coherente via ETAPI (jamais de copie SQLite a chaud) # 2) la sort du dossier Docker vers /volume1/backups/trilium, horodatee # 3) verifie l integrite du fichier copie # 4) garde les 30 derniers # Sortie : "OK ..." ou "ERREUR ..." (pour alerte mail DSM). BASE=/volume1/homes/Master/App/Context_continuity PY=$BASE/venv/bin/python3 SRC=/volume1/docker/trilium/backup/backup-quotidien.db DEST=/volume1/backups/trilium URL=http://localhost:4292 GARDER=30 STAMP=$(date +%Y%m%d_%H%M) CIBLE=$DEST/trilium_$STAMP.db TOKEN=$(grep '^TRILIUM_TOKEN=' $BASE/.env | cut -d= -f2- | tr -d '"' | tr -d "'") if [ -z "$TOKEN" ]; then echo "ERREUR token ETAPI introuvable dans $BASE/.env"; exit 1; fi mkdir -p $DEST # 1. Demander a Trilium de regenerer la copie (nom dedie : n ecrase pas daily/weekly/monthly) CODE=$(curl -s -o /dev/null -w "%{http_code}" -X PUT "$URL/etapi/backup/quotidien" -H "Authorization: $TOKEN" --max-time 60) if [ "$CODE" != "204" ]; then echo "ERREUR ETAPI backup HTTP $CODE"; exit 1; fi # 2. Verifier que le fichier existe et vient d etre ecrit, puis le sortir du dossier Docker if [ ! -f "$SRC" ]; then echo "ERREUR fichier source absent : $SRC"; exit 1; fi find "$SRC" -mmin -5 | grep -q . || { echo "ERREUR source non regeneree (plus de 5 min)"; exit 1; } cp "$SRC" "$CIBLE" || { echo "ERREUR copie vers $CIBLE"; exit 1; } # 3. Verifier l integrite de la COPIE (un backup non verifie n est pas un backup) INTEG=$($PY -c "import sqlite3,sys try: c=sqlite3.connect('$CIBLE') r=c.execute('PRAGMA integrity_check').fetchone()[0] n=c.execute('SELECT count(*) FROM notes').fetchone()[0] c.close() print('%s|%d' % (r,n)) except Exception as e: print('erreur|%s' % e)") RESU=$(echo "$INTEG" | cut -d'|' -f1) NBNOTES=$(echo "$INTEG" | cut -d'|' -f2) if [ "$RESU" != "ok" ]; then rm -f "$CIBLE"; echo "ERREUR integrite KO ($INTEG) - copie supprimee"; exit 1; fi # 4. Rotation : garder les N plus recents cd $DEST || exit 1 ls -1t trilium_*.db 2>/dev/null | tail -n +$((GARDER+1)) | while read f; do rm -f "$f"; done chmod 600 "$CIBLE" # 5. Push off-site vers Proton Drive (n echoue PAS le backup local si KO) REMOTE="proton:Backups/trilium" OFFSITE="offsite:KO" if command -v rclone >/dev/null 2>&1; then if rclone copy "$CIBLE" "$REMOTE/" --max-duration 5m >/dev/null 2>&1; then # rotation distante : garder les N plus recents rclone lsf "$REMOTE/" 2>/dev/null | grep '^trilium_.*\.db$' | sort -r | tail -n +$((GARDER+1)) | while read f; do rclone deletefile "$REMOTE/$f" >/dev/null 2>&1 done NBREMOTE=$(rclone lsf "$REMOTE/" 2>/dev/null | grep -c '^trilium_.*\.db$') OFFSITE="offsite:OK($NBREMOTE)" fi else OFFSITE="offsite:rclone-absent" fi TAILLE=$(du -h "$CIBLE" | cut -f1) RESTANT=$(ls -1 trilium_*.db 2>/dev/null | wc -l) echo "OK $CIBLE ($TAILLE, $NBNOTES notes, $RESTANT locaux, $OFFSITE)"