Toto repo ma novou cistou historii (1 commit). Predchozi historie byla smazana, protoze obsahovala osobni udaje (IP, emaily, hesla, klice, tokeny, Volume cesty). Soubory v aktualnim commit uz tyto udaje neobsahuji (nahradeno placeholdery <server-ip>, CHANGE_ME, admin@example.com atd.). Pokud chces zpet kompletni seznam starych commitu, mas je ve svem lokalnim working tree - git log --all ukaze prazdno (protoze jsme smazali .git/ a zacali znovu).
342 lines
11 KiB
Bash
342 lines
11 KiB
Bash
#!/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-<TS>/ (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:-<docker-projects-root>}"
|
|
BACKUP_ROOT="${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: <project>.tar.zst v Dockery/
|
|
# Volume: <volname>.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/<project>/...
|
|
# Extrahujeme s --strip-components=3, abychom získali přímo <project>/...
|
|
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 |