#!/usr/bin/env bash # Полный консистентный онлайн-бэкап прода (БД + uploads + achievements) с работающего # контейнера + оффсайт-копия на VPS. Запуск по cron на хосте Pi, например ежедневно в 04:00: # 0 4 * * * /home/pi/ForbidenStarsApp/scripts/backup.sh >> /var/log/fs-backup.log 2>&1 # # Настройки берутся из корневого .env (BACKUP_*), их можно переопределить переменными # окружения. Если BACKUP_VPS_HOST пуст — делается только локальная копия в backups/. set -euo pipefail PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" COMPOSE="docker compose -f $PROJECT_DIR/docker-compose.yml" SERVICE="app" TS="$(date +%Y%m%d_%H%M%S)" # Безопасно прочитать ОДИН ключ из .env (НЕ source: в .env есть спецсимволы # & и т.п.). read_env() { # $1=ключ $2=дефолт local val val="$(grep -E "^$1=" "$PROJECT_DIR/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d '\r')" val="${val%\"}"; val="${val#\"}" # снять обрамляющие кавычки, если есть printf '%s' "${val:-$2}" } BACKUP_DIR="${BACKUP_DIR:-$PROJECT_DIR/backups}" KEEP_LOCAL="${BACKUP_KEEP_LOCAL:-$(read_env BACKUP_KEEP_LOCAL 14)}" KEEP_REMOTE="${BACKUP_KEEP_REMOTE:-$(read_env BACKUP_KEEP_REMOTE 30)}" VPS_HOST="${BACKUP_VPS_HOST:-$(read_env BACKUP_VPS_HOST "")}" VPS_USER="${BACKUP_VPS_USER:-$(read_env BACKUP_VPS_USER backup)}" VPS_DIR="${BACKUP_VPS_DIR:-$(read_env BACKUP_VPS_DIR /srv/fs-backups)}" VPS_KEY="${BACKUP_VPS_KEY:-$(read_env BACKUP_VPS_KEY "$PROJECT_DIR/deploy/backup/id_backup")}" mkdir -p "$BACKUP_DIR" STAGE="$(mktemp -d)" trap 'rm -rf "$STAGE"' EXIT CID="$($COMPOSE ps -q "$SERVICE")" if [ -z "$CID" ]; then echo "[backup] ОШИБКА: контейнер '$SERVICE' не запущен." >&2 exit 1 fi echo "[backup] Консистентный снимок БД…" $COMPOSE exec -T "$SERVICE" python -c "import sqlite3; s=sqlite3.connect('/data/forbidden_stars.db'); d=sqlite3.connect('/data/backup_tmp.db'); s.backup(d); d.close(); s.close()" docker cp "$CID:/data/backup_tmp.db" "$STAGE/forbidden_stars.db" $COMPOSE exec -T "$SERVICE" rm -f /data/backup_tmp.db echo "[backup] Копирование медиа и титулов…" docker cp "$CID:/data/uploads" "$STAGE/uploads" docker cp "$CID:/data/achievements" "$STAGE/achievements" ARCHIVE="$BACKUP_DIR/fs_$TS.tar.gz" tar -czf "$ARCHIVE" -C "$STAGE" forbidden_stars.db uploads achievements echo "[backup] Локальный архив: $ARCHIVE ($(du -h "$ARCHIVE" | cut -f1))" # Локальная ротация: оставить последние $KEEP_LOCAL (архив только что создан → ≥1 файл). ls -1t "$BACKUP_DIR"/fs_*.tar.gz 2>/dev/null | tail -n +"$((KEEP_LOCAL + 1))" | xargs -r rm -f # Оффсайт-копия на VPS (push: Pi за CGNAT, входящих нет). if [ -n "$VPS_HOST" ]; then if [ ! -f "$VPS_KEY" ]; then echo "[backup] ОШИБКА: ключ $VPS_KEY не найден — оффсайт-копия пропущена." >&2 exit 1 fi echo "[backup] Отправка на VPS $VPS_USER@$VPS_HOST:$VPS_DIR …" SSH_OPTS=(-i "$VPS_KEY" -o StrictHostKeyChecking=accept-new -o BatchMode=yes) scp "${SSH_OPTS[@]}" "$ARCHIVE" "$VPS_USER@$VPS_HOST:$VPS_DIR/" # Удалённая ротация (sh на VPS без pipefail → пустой ls не уронит команду). ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" \ "ls -1t '$VPS_DIR'/fs_*.tar.gz 2>/dev/null | tail -n +$((KEEP_REMOTE + 1)) | xargs -r rm -f" echo "[backup] Оффсайт-копия отправлена." else echo "[backup] BACKUP_VPS_HOST не задан — только локальная копия." fi echo "[backup] Готово."