#!/usr/bin/env bash # # docker-restore.sh — bezpečná obnova Docker projektů z tar.zst zálohy # # - zobrazí dostupné zálohy v $BACKUP_ROOT # - vyžádá potvrzení (nebo --auto) # - zastaví vybrané projekty # - přesune současná data do _pre-restore-/ (NE rm -rf!) # - obnoví data ze zálohy # - vyčistí AppleDouble _* před docker compose up -d # - spustí kontejnery a ověří zdraví # # Použití: # ./docker-restore.sh # interaktivně: vybere zálohu + projekty # ./docker-restore.sh --list # jen seznam dostupných záloh # ./docker-restore.sh 2026-07-29_19-21-00 # konkrétní záloha, interaktivní výběr projektů # ./docker-restore.sh 2026-07-29_19-21-00 nginx vaultwarden # konkrétní záloha + projekty # ./docker-restore.sh --auto 2026-07-29_19-21-00 nginx # bez ptaní # ./docker-restore.sh --help # nápověda # set -euo pipefail DOCKERY="${DOCKERY:-}" BACKUP_ROOT="${BACKUP_ROOT:-}" HEALTH_TIMEOUT="${HEALTH_TIMEOUT:-60}" AUTO=0 LIST_ONLY=0 TS_ARG="" PROJECTS_RAW=() usage() { sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//' } for arg in "$@"; do case "$arg" in --help|-h) usage; exit 0 ;; --auto|-y) AUTO=1 ;; --list|-l) LIST_ONLY=1 ;; --*) echo "Unknown flag: $arg" >&2; exit 1 ;; *) if [ -z "$TS_ARG" ]; then TS_ARG="$arg" else PROJECTS_RAW+=("$arg") fi ;; esac done # Barevné výstupy if [ -t 1 ]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; NC='' fi log() { printf '%s[%s]%s %s\n' "$GREEN" "$(date '+%H:%M:%S')" "$NC" "$*"; } warn() { printf '%s[%s]%s %s\n' "$YELLOW" "$(date '+%H:%M:%S')" "$NC" "$*"; } err() { printf '%s[%s]%s %s\n' "$RED" "$(date '+%H:%M:%S')" "$NC" "$*"; } confirm() { if [ "$AUTO" = "1" ]; then return 0; fi printf '%s%s%s ' "$YELLOW" "$1" "$NC" read -r ans case "$ans" in y|Y|yes|YES) return 0 ;; *) printf 'Zrušeno.\n'; exit 1 ;; esac } # Kontrola if ! docker info >/dev/null 2>&1; then err "Docker není dostupný." exit 1 fi if [ ! -d "$DOCKERY" ]; then err "Dockery adresář neexistuje: $DOCKERY" exit 1 fi if [ ! -d "$BACKUP_ROOT" ]; then err "Backup adresář neexistuje: $BACKUP_ROOT" exit 1 fi # === Seznam dostupných záloh === list_backups() { printf '%s%s%s\n' "$BLUE" "=== DOSTUPNÉ ZÁLOHY ===" "$NC" found=0 while IFS= read -r d; do [ -d "$d" ] || continue ts=$(basename "$d") archives=$(/bin/ls "$d"/*.tar.zst 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') size=$(/usr/bin/du -sh "$d" 2>/dev/null | /usr/bin/awk '{print $1}') printf ' %s %s archivů %s\n' "$ts" "$archives" "$size" found=$((found + 1)) done < <(/usr/bin/find "$BACKUP_ROOT" -maxdepth 1 -mindepth 1 -type d \ | /usr/bin/sort -r) if [ "$found" -eq 0 ]; then printf ' (žádné)\n' fi } # Zobrazit seznam, pokud --list if [ "$LIST_ONLY" = "1" ]; then list_backups exit 0 fi # === Vyber zálohu === if [ -z "$TS_ARG" ]; then list_backups printf '\n%sZadejte timestamp zálohy (např. 2026-07-29_19-21-00):%s ' "$YELLOW" "$NC" read -r TS_ARG fi BACKUP_DIR="$BACKUP_ROOT/$TS_ARG" if [ ! -d "$BACKUP_DIR" ]; then err "Záloha neexistuje: $BACKUP_DIR" list_backups exit 1 fi log "Vybraná záloha: $BACKUP_DIR" log "Archivy v záloze:" /bin/ls -1 "$BACKUP_DIR"/*.tar.zst 2>/dev/null | /usr/bin/sed 's/^/ /' || { err "Záloha neobsahuje žádné archivy" exit 1 } # === Detekce projektů v záloze === DETECTED=() for archive in "$BACKUP_DIR"/*.tar.zst; do name=$(basename "$archive" .tar.zst) # Rozlišení: project archive vs volume archive # Project: .tar.zst v Dockery/ # Volume: .tar.zst v named volumes if [ -d "$DOCKERY/$name" ] && compgen -G "$DOCKERY/$name/docker-compose.y*ml" > /dev/null; then DETECTED+=("project:$name") else # Může to být volume; docker volume ls to ověří if docker volume inspect "$name" >/dev/null 2>&1; then DETECTED+=("volume:$name") else DETECTED+=("unknown:$name") fi fi done log "" log "Položky v záloze:" for d in "${DETECTED[@]}"; do printf ' %s\n' "$d"; done # === Vyber projekty k obnově === if [ "${#PROJECTS_RAW[@]}" -eq 0 ]; then PROJECTS=() for d in "${DETECTED[@]}"; do type=${d%%:*} name=${d#*:} if [ "$type" = "project" ]; then PROJECTS+=("$name") fi done if [ "${#PROJECTS[@]}" -eq 0 ]; then err "V záloze nebyly nalezeny žádné projekty k obnově" exit 1 fi printf '\n%sDostupné projekty v této záloze:%s\n' "$BLUE" "$NC" for i in "${!PROJECTS[@]}"; do printf ' %d) %s\n' $((i+1)) "${PROJECTS[$i]}" done printf '%sVyberte projekty (např. 1 3 nebo "all"):%s ' "$YELLOW" "$NC" read -r sel case "$sel" in all|"") SELECTED=("${PROJECTS[@]}") ;; *) SELECTED=() for n in $sel; do idx=$((n - 1)) if [ "$idx" -ge 0 ] && [ "$idx" -lt "${#PROJECTS[@]}" ]; then SELECTED+=("${PROJECTS[$idx]}") fi done ;; esac else SELECTED=("${PROJECTS_RAW[@]}") fi if [ "${#SELECTED[@]}" -eq 0 ]; then err "Nebyly vybrány žádné projekty" exit 1 fi # === Plán obnovy === TS_NOW="$(date +%Y-%m-%d_%H-%M-%S)" printf '\n%s%s%s\n' "$BLUE" "=== PLÁN OBNOVY ===" "$NC" printf 'Záloha: %s\n' "$BACKUP_DIR" printf 'Projekty: %s\n' "${SELECTED[*]}" printf 'Pre-restore: %s/_pre-restore-%s/\n' "$DOCKERY" "$TS_NOW" printf '\n' confirm "Pokračovat v obnově? (PŘEPSOU se současná data projektů!) [y/N]" # === Stop kontejnerů vybraných projektů === log "" log "=== STOP: zastavuji kontejnery vybraných projektů ===" for project in "${SELECTED[@]}"; do cd "$DOCKERY/$project" if docker compose ps -q 2>/dev/null | grep -q .; then log " [$project] docker compose stop" docker compose stop 2>&1 || warn " [$project] stop selhal" fi done # === Move-aside současných dat === log "" log "=== MOVE-ASIDE: přesouvám současná data do _pre-restore-$TS_NOW/ ===" for project in "${SELECTED[@]}"; do SRC="$DOCKERY/$project" DEST="$DOCKERY/_pre-restore-$TS_NOW/$project" mkdir -p "$DEST" # Najdi velké podadresáře a přesuň je (NEVYMAŽ!) for sub in $(/bin/ls -A "$SRC" 2>/dev/null); do # Přeskoč .DS_Store, samotný _pre-restore-*, docker-compose soubory case "$sub" in _pre-restore-*) continue ;; docker-compose.y*ml) continue ;; .env|.env.*) continue ;; # .env necháme — je to runtime konfigurace .DS_Store) continue ;; esac if [ -e "$SRC/$sub" ]; then log " [$project] mv $sub → _pre-restore-$TS_NOW/$project/" mv "$SRC/$sub" "$DEST/$sub" 2>&1 || warn " mv $sub selhal" fi done done # === Restore ze zálohy === log "" log "=== RESTORE: extrahuji ze zálohy ===" FAILED=() for project in "${SELECTED[@]}"; do ARCHIVE="$BACKUP_DIR/${project}.tar.zst" if [ ! -f "$ARCHIVE" ]; then warn "[$project] archiv neexistuje: $ARCHIVE (přeskakuji)" FAILED+=("$project:no-archive") continue fi log " [$project] extrahuji do $DOCKERY/$project/" # Archiv má cesty jako Volumes/SSD-data/Dockery//... # Extrahujeme s --strip-components=3, abychom získali přímo /... cd "$DOCKERY" if /opt/homebrew/bin/zstd -d -c "$ARCHIVE" \ | /usr/bin/tar --strip-components=3 -xf - 2>>"$BACKUP_DIR/.restore-errors-$TS_NOW"; then log " OK" else err " extrakce selhala pro $project" FAILED+=("$project:extract-failed") fi done # === Vyčistit AppleDouble _* (prevence MariaDB/PG smyček) === log "" log "=== CLEAN APPLE DOUBLE: mažu ._* před startem ===" for project in "${SELECTED[@]}"; do count=$(/usr/bin/find "$DOCKERY/$project" -name '._*' -type f 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') if [ "$count" -gt 0 ]; then log " [$project] nalezeno $count ._* souborů, mažu" /usr/bin/find "$DOCKERY/$project" -name '._*' -type f -delete 2>/dev/null || true fi # .DS_Store dscount=$(/usr/bin/find "$DOCKERY/$project" -name '.DS_Store' -type f 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') if [ "$dscount" -gt 0 ]; then log " [$project] nalezeno $dscount .DS_Store, mažu" /usr/bin/find "$DOCKERY/$project" -name '.DS_Store' -type f -delete 2>/dev/null || true fi done # === Up kontejnerů === log "" log "=== UP: spouštím kontejnery ===" for project in "${SELECTED[@]}"; do cd "$DOCKERY/$project" log " [$project] docker compose up -d" docker compose up -d 2>&1 | /usr/bin/head -10 || warn " [$project] up -d selhal" done # === Health check === log "" log "=== HEALTH CHECK: čekám na zdraví (max ${HEALTH_TIMEOUT}s) ===" sleep 10 elapsed=10 all_healthy=1 while [ "$elapsed" -lt "$HEALTH_TIMEOUT" ]; do sleep 5 elapsed=$((elapsed + 5)) for project in "${SELECTED[@]}"; do for c in $(docker compose -f "$DOCKERY/$project/docker-compose.yml" ps -a --format '{{.Names}}' 2>/dev/null); do status=$(docker inspect --format='{{.State.Status}}' "$c" 2>/dev/null) health=$(docker inspect --format='{{.State.Health.Status}}' "$c" 2>/dev/null) if [ "$status" != "running" ]; then log " [$project/$c] state=$status health=$health" all_healthy=0 fi done done # Pokud všechny kontejnery running a žádné health check nehlásí starting/unhealthy, ukončíme starting=$(docker ps --format '{{.Names}}' \ | while read -r c; do docker inspect --format='{{.State.Health.Status}}' "$c" 2>/dev/null; done \ | /usr/bin/grep -c '^starting$' || true) if [ "$starting" = "0" ]; then break fi done # === Report === log "" log "======================================" log " SHRNUTÍ OBNOVY" log "======================================" log "Obnoveno:" for p in "${SELECTED[@]}"; do log " ✓ $p"; done if [ "${#FAILED[@]}" -gt 0 ]; then err "Selhalo:" for f in "${FAILED[@]}"; do err " ✗ $f"; done fi log "" log "Současná data jsou v: $DOCKERY/_pre-restore-$TS_NOW/" log "Po ověření můžete tento adresář ručně smazat:" log " rm -rf $DOCKERY/_pre-restore-$TS_NOW" log "" if [ "$all_healthy" = "1" ]; then log "Všechny kontejnery běží." else warn "Některé kontejnery nejsou v pořádku — zkontrolujte logy." fi