Files
ForbiddenStarsApp/scripts/fs-backup.sh
T
NotBigGhostandClaude Opus 5 d764ff86d3 Бэкап: скрипты управления с ПК
scripts/fs-backup.ps1 (ASCII, Windows PowerShell 5.1) и scripts/fs-backup.sh (Linux/macOS/
Git Bash) — одинаковые команды к контейнеру backup прода на Pi по SSH или к локальному
тест-клону (-Target test / --test): status, list, now [--tag], verify;
pull — export в файл на Pi, scp в backups/, сверка sha256 и проверка содержимого tar
(бинарные данные не идут через пайпы PowerShell — они их портят);
restore-test — учебное восстановление архива (в т.ч. старого fs_*.tar.gz) в тест-клон.
Настройки BACKUP_PI_SSH / BACKUP_PI_DIR из .env, переменная окружения важнее.
Вызовы нативных команд устойчивы к перенаправлению stderr в PS 5.1. #64

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013jBxs9nBCk5nBdTLzcGz91
2026-09-14 01:12:41 +03:00

139 lines
7.0 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Бэкапы Forbidden Stars с ПК (Linux / macOS / Git Bash). Команды уходят в контейнер backup
# прода на Pi по SSH или (с --test) в локальный тест-клон (docker-compose.test.yml).
# На Windows удобнее scripts/fs-backup.ps1 — поведение то же. Инструкция: deploy/backup/README.md
#
# scripts/fs-backup.sh status состояние бэкапов на Pi
# scripts/fs-backup.sh list [vps] хронология снимков
# scripts/fs-backup.sh now [--tag before-update] сделать снимок сейчас
# scripts/fs-backup.sh verify проверить целостность данных
# scripts/fs-backup.sh pull [<id>|latest] [--repo vps]
# скачать снимок в backups/ (сверка sha256)
# scripts/fs-backup.sh restore-test <файл.tar|.tar.gz>
# учебное восстановление в локальный тест-клон
# --test первым аргументом — status/list/now/verify/pull для локального тест-клона
#
# Настройки — из корневого .env (переменная окружения с тем же именем важнее):
# BACKUP_PI_SSH как зайти на Pi по SSH, например pi@192.168.1.10 (или Host из ~/.ssh/config)
# BACKUP_PI_DIR папка на Pi с docker-compose.yml и .env (по умолчанию ~/forbidden-stars)
set -euo pipefail
PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
TEST_COMPOSE="$PROJECT_DIR/docker-compose.test.yml"
export MSYS_NO_PATHCONV=1 # Git Bash: не переписывать /import/... в аргументах docker
die() { echo "ОШИБКА: $*" >&2; exit 1; }
setting() { # setting <ключ> <по умолчанию>: окружение, затем .env (последнее присваивание)
local val="${!1:-}"
if [ -z "$val" ] && [ -f "$PROJECT_DIR/.env" ]; then
val="$(grep -E "^\s*$1\s*=" "$PROJECT_DIR/.env" | tail -n 1 | cut -d= -f2- | sed -e 's/\s*#.*$//' -e 's/^\s*//' -e 's/\s*$//' | tr -d '\r"')"
fi
printf '%s' "${val:-$2}"
}
PI_SSH="$(setting BACKUP_PI_SSH "")"
# shellcheck disable=SC2088 # тильда намеренно не раскрывается здесь — её раскроет shell на Pi
PI_DIR="$(setting BACKUP_PI_DIR "~/forbidden-stars")"
TARGET="pi"
if [ "${1:-}" = "--test" ]; then TARGET="test"; shift; fi
native_path() { if command -v cygpath >/dev/null 2>&1; then cygpath -w "$1"; else printf '%s' "$1"; fi; }
sha256() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1; else shasum -a 256 "$1" | cut -d' ' -f1; fi; }
pi() { # pi <shell-команда>: выполнить на Pi в папке прода
[ -n "$PI_SSH" ] || die "задайте BACKUP_PI_SSH в .env (как вы заходите на Pi, например pi@192.168.1.10) — README, шаг 6."
ssh -o ConnectTimeout=15 "$PI_SSH" "cd $PI_DIR && $1"
}
tc() { docker compose -f "$(native_path "$TEST_COMPOSE")" "$@"; }
start_test_backup() {
if [ -z "$(tc ps -q backup 2>/dev/null)" ]; then
echo "Запускаю контейнер backup тест-клона…"
tc up -d --build backup
fi
}
fs() { # fs <аргументы fs-backup…>
if [ "$TARGET" = test ]; then
start_test_backup
tc exec -T backup fs-backup "$@"
else
pi "docker compose exec -T backup fs-backup $*"
fi
}
cmd_pull() {
local snap=latest repo=local
while [ $# -gt 0 ]; do
case "$1" in
--repo) repo="${2:?--repo требует значение}"; shift ;;
*) snap="$1" ;;
esac
shift
done
mkdir -p "$PROJECT_DIR/backups"
local info id stamp local_file partial expected actual
info="$(fs info "$snap" --repo "$repo" | tail -n 1)"
id="$(printf '%s' "$info" | awk '{ print $1 }')"
stamp="$(printf '%s' "$info" | awk '{ print $2 }')"
[ -n "$id" ] && [ -n "$stamp" ] || die "неожиданный ответ fs-backup info: '$info'"
local_file="$PROJECT_DIR/backups/fs_${stamp}_${id}.tar"
if [ -f "$local_file" ]; then echo "Уже скачан: $local_file"; return 0; fi
partial="$local_file.part"
echo "Снимок $id ($repo) -> $local_file"
if [ "$TARGET" = test ]; then
start_test_backup
local tmp="/tmp/fs-backup/export-$id.tar"
expected="$(tc exec -T backup sh -c "fs-backup export $id --repo $repo > $tmp && sha256sum $tmp" | tail -n 1 | cut -d' ' -f1)"
tc cp "backup:$tmp" "$(native_path "$partial")" || { tc exec -T backup rm -f "$tmp"; die "копирование из контейнера не удалось"; }
tc exec -T backup rm -f "$tmp"
else
local remote="fs-export-$id.tar"
expected="$(pi "docker compose exec -T backup fs-backup export $id --repo $repo > ~/$remote && sha256sum ~/$remote" | tail -n 1 | cut -d' ' -f1)"
scp -o ConnectTimeout=15 "$PI_SSH:$remote" "$partial" || { pi "rm -f ~/$remote"; die "scp не удался"; }
pi "rm -f ~/$remote"
fi
actual="$(sha256 "$partial")"
if [ "$expected" != "$actual" ]; then
rm -f "$partial"
die "контрольная сумма не совпала (ожидалась $expected, получена $actual) — файл удалён, повторите pull."
fi
mv "$partial" "$local_file"
tar -tf "$local_file" | grep -qx 'forbidden_stars.db' || die "в архиве нет forbidden_stars.db: $local_file"
echo "OK: $local_file ($(du -h "$local_file" | cut -f1), $(tar -tf "$local_file" | grep -vc '/$') файлов, sha256 сверена)"
}
cmd_restore_test() {
local file="${1:-}"
[ -n "$file" ] || die "укажите архив: scripts/fs-backup.sh restore-test backups/fs_....tar"
[ -f "$file" ] || die "файл не найден: $file"
local in_container=/import/restore-test.archive # tar или tar.gz — формат fs-backup определит сам
echo "Учебное восстановление $file в ЛОКАЛЬНЫЙ ТЕСТ-КЛОН (прод не затрагивается)."
start_test_backup
tc stop app
tc cp "$(native_path "$file")" "backup:$in_container"
local rc=0
tc exec -T backup fs-backup import "$in_container" --yes || rc=$?
tc exec -T backup rm -f "$in_container"
[ "$rc" -eq 0 ] || die "импорт не удался (код $rc) — данные тест-клона не изменены."
tc up -d app
echo "Готово: тест-клон работает на восстановленных данных."
echo " На https://forbidden-stars.ru: docker compose -f docker-compose.test.yml up -d (или ./run.sh при APP_ENV=test)"
}
cmd="${1:-help}"
[ $# -eq 0 ] || shift
case "$cmd" in
status) fs status ;;
list) fs list "${1:-local}" ;;
now) fs run "$@" ;;
verify) fs verify ;;
pull) cmd_pull "$@" ;;
restore-test) cmd_restore_test "$@" ;;
*) sed -n '2,19p' "$0" | sed 's/^# \{0,1\}//' ;;
esac