#!/usr/bin/env bash # Seiche — sauvegarde SQLite (en ligne, cohérente, sans arrêter le serveur). # # Usage : # ./scripts/backup.sh [répertoire_de_sortie] # défaut : ./backups # # Prérequis : le conteneur `seiche` tourne (docker compose up -d). # # Principe : # 1. `VACUUM INTO` dans le conteneur (snapshot cohérent garanti par SQLite, # le serveur continue de servir pendant l'opération), # 2. copie HORS du volume (un backup dans le même volume n'est pas un backup), # 3. nettoyage du fichier temporaire dans le volume. set -euo pipefail cd "$(dirname "$0")/.." OUT_DIR="${1:-backups}" mkdir -p "$OUT_DIR" TS="$(date +%Y%m%d-%H%M%S)" HOST_FILE="$OUT_DIR/seiche-$TS.db" TMP_IN_CONTAINER="/data/.seiche-backup-tmp.db" # 1. Snapshot cohérente dans le conteneur (libsql = dépendance du serveur, # résolu depuis le WORKDIR /app/server ; l'exécution est en CJS stdin, # d'où l'import dynamique). docker compose exec -T seiche node <<'NODE' import("@libsql/client").then(async (m) => { const c = m.createClient({ url: "file:/data/seiche.db" }); await c.execute("VACUUM INTO '/data/.seiche-backup-tmp.db'"); await c.close(); }).catch((e) => { console.error(e); process.exit(1); }); NODE # 2. Copie hors du volume. docker compose cp "seiche:$TMP_IN_CONTAINER" "$HOST_FILE" # 3. Nettoyage dans le volume. docker compose exec -T seiche rm -f "$TMP_IN_CONTAINER" # 4. Vérification minimale : en-tête SQLite + fichier non vide. MAGIC="$(head -c 15 "$HOST_FILE" 2>/dev/null || true)" if [ "$MAGIC" != "SQLite format 3" ] || [ ! -s "$HOST_FILE" ]; then echo "ERREUR : backup invalide ($HOST_FILE), suppression." >&2 rm -f "$HOST_FILE" exit 1 fi echo "OK : $HOST_FILE ($(du -h "$HOST_FILE" | cut -f1))"