Files
grendervill 3151327f6e feat(ops): offsite-бэкапы и проверки профилей
- deploy/backup.sh: BACKUP_OFFSITE_TARGET (rsync или rclone:remote:path)
  выгружает готовый архив после локальной ротации; неудача выгрузки только
  предупреждает. Ротация на приёмнике — опционально
  (BACKUP_OFFSITE_KEEP_DAYS, rclone delete --min-age). Имя архива получает
  суффикс, если бэкап запущен дважды в одну секунду.
- deploy/install.sh: offsite-настройки в .env (BACKUP_OFFSITE_TARGET,
  BACKUP_OFFSITE_KEEP_DAYS) отдельным разделом; детектор незаполненных
  плейсхолдеров учитывает цифры в именах (FAIL2BAN_ENABLED); dry-run больше не
  требует прав на /opt/glchat — явность GLCHAT_ETC_DIR фиксируется до
  подключения common.sh, иначе ветка временного каталога была недостижима.
- scripts/test-profiles.sh: сверка таблицы профилей 10.10 и одинакового набора
  PROFILE_* (46 проверок), make profiles-test / ops-test.
- scripts/test-backup.sh: бэкап и offsite на синтетическом инстансе — локальный
  rsync-приёмник, шим rclone, недоступный хост, нечисловой срок (12 проверок),
  make backup-test.
- scripts/test-install.sh: проверка offsite-ключей в .env после установки.
2026-09-22 18:44:50 +03:00

156 lines
6.4 KiB
Bash
Executable File
Raw Permalink 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
# glchat: бэкап БД и файлов с age-шифрованием (AGENT.md 10.8).
set -euo pipefail
GLCHAT_ETC_DIR="${GLCHAT_ETC_DIR:-/opt/glchat}"
GLCHAT_DATA_DIR="${GLCHAT_DATA_DIR:-${GLCHAT_ETC_DIR}/data}"
ENV_FILE="${GLCHAT_ETC_DIR}/.env"
BACKUP_DIR="${GLCHAT_DATA_DIR}/backups"
KEEP_DAYS_DEFAULT=7
log() { printf '[backup] %s\n' "$*"; }
warn() { printf '[backup] внимание: %s\n' "$*" >&2; }
die() {
printf '[backup] ошибка: %s\n' "$*" >&2
exit 1
}
[ -f "$ENV_FILE" ] || die "не найден ${ENV_FILE}"
# shellcheck disable=SC1090
set -a && . "$ENV_FILE" && set +a
KEEP_DAYS="${BACKUP_KEEP_DAYS:-$KEEP_DAYS_DEFAULT}"
AGE_RECIPIENT="${AGE_RECIPIENT:-}"
# Выгрузка копии наружу: rsync-цель (user@host:/path) или `rclone:remote:path`
# (AGENT.md 10.8, опциональный offsite). Ошибка выгрузки не отменяет локальный
# бэкап: о ней пишем предупреждение.
OFFSITE_TARGET="${BACKUP_OFFSITE_TARGET:-}"
# Ротация на принимающей стороне — только по явной настройке: пусто значит
# «копим копии, чистим вручную».
OFFSITE_KEEP_DAYS="${BACKUP_OFFSITE_KEEP_DAYS:-}"
case "$OFFSITE_KEEP_DAYS" in
"") ;;
*[!0-9]*)
warn "BACKUP_OFFSITE_KEEP_DAYS='${OFFSITE_KEEP_DAYS}' не число — offsite-ротация выключена"
OFFSITE_KEEP_DAYS=""
;;
esac
STAMP="$(date -u +%Y%m%dT%H%M%SZ)"
# Два запуска в одну секунду не должны затирать друг друга: имя с суффиксом.
SUFFIX=""
suffix_n=1
while [ -e "${BACKUP_DIR}/glchat-${STAMP}${SUFFIX}.tar.gz" ] ||
[ -e "${BACKUP_DIR}/glchat-${STAMP}${SUFFIX}.tar.gz.age" ]; do
SUFFIX="-${suffix_n}"
suffix_n=$((suffix_n + 1))
done
WORK_DIR="$(mktemp -d)"
ARCHIVE="${BACKUP_DIR}/glchat-${STAMP}${SUFFIX}.tar.gz"
TARGET="${ARCHIVE}.age"
# ARCHIVE_OR_TARGET — готовый файл бэкапа (с шифрованием или без): его выгружает
# offsite-копирование.
ARCHIVE_OR_TARGET=""
cleanup() { rm -rf "$WORK_DIR"; }
trap cleanup EXIT
mkdir -p "$BACKUP_DIR"
chmod 700 "$BACKUP_DIR"
log "дамп SQLite (sqlite3 .backup)"
if command -v sqlite3 >/dev/null 2>&1; then
sqlite3 "${GLCHAT_DATA_DIR}/db/glchat.db" ".backup '${WORK_DIR}/glchat.db'" ||
die "не удалось сделать дамп базы"
else
docker exec glchat-app true 2>/dev/null || die "sqlite3 не установлен и контейнер недоступен"
docker cp glchat-app:/app/data/db/glchat.db "${WORK_DIR}/glchat.db"
fi
log "копируем файлы"
mkdir -p "${WORK_DIR}/files"
if [ -d "${GLCHAT_DATA_DIR}/files" ]; then
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete "${GLCHAT_DATA_DIR}/files/" "${WORK_DIR}/files/"
else
cp -a "${GLCHAT_DATA_DIR}/files/." "${WORK_DIR}/files/"
fi
fi
log "сохраняем конфиги (без секретов)"
mkdir -p "${WORK_DIR}/config"
cp -a "${GLCHAT_ETC_DIR}/compose.yaml" "${WORK_DIR}/config/" 2>/dev/null || true
cp -a "${GLCHAT_ETC_DIR}/livekit" "${WORK_DIR}/config/" 2>/dev/null || true
cp -a "${GLCHAT_ETC_DIR}/external-proxy.conf" "${WORK_DIR}/config/" 2>/dev/null || true
cat >"${WORK_DIR}/MANIFEST.txt" <<EOF
glchat backup
created_at=${STAMP}
domain=${DOMAIN:-unknown}
version=${APP_VERSION:-unknown}
profile=${PROFILE:-unknown}
EOF
log "упаковываем"
tar -czf "$ARCHIVE" -C "$WORK_DIR" .
if [ -n "$AGE_RECIPIENT" ]; then
command -v age >/dev/null 2>&1 || die "age не установлен, а AGE_RECIPIENT задан"
age -r "$AGE_RECIPIENT" -o "$TARGET" "$ARCHIVE" || die "age-шифрование не удалось"
rm -f "$ARCHIVE"
chmod 600 "$TARGET"
ARCHIVE_OR_TARGET="$TARGET"
log "готово: ${TARGET} ($(du -h "$TARGET" | cut -f1))"
else
chmod 600 "$ARCHIVE"
ARCHIVE_OR_TARGET="$ARCHIVE"
log "готово (БЕЗ шифрования, AGE_RECIPIENT не задан): ${ARCHIVE}"
fi
log "ротация: оставляем ${KEEP_DAYS} дн."
find "$BACKUP_DIR" -maxdepth 1 -name 'glchat-*.tar.gz*' -mtime "+${KEEP_DAYS}" -print -delete || true
# Offsite идёт после локальной ротации: наружу уезжает уже актуальный набор.
# Неудача выгрузки не отменяет локальный бэкап — только предупреждение.
if [ -n "$OFFSITE_TARGET" ]; then
log "выгружаем копию: ${OFFSITE_TARGET}"
offsite_ok=0
case "$OFFSITE_TARGET" in
rclone:*)
remote="${OFFSITE_TARGET#rclone:}"
if command -v rclone >/dev/null 2>&1; then
rclone copy "$ARCHIVE_OR_TARGET" "$remote" && offsite_ok=1
else
warn "rclone не установлен: offsite-копия не сделана"
fi
;;
*)
if command -v rsync >/dev/null 2>&1; then
# Цель — каталог на приёмнике; без --delete копии только копятся.
rsync -az --partial "$ARCHIVE_OR_TARGET" "${OFFSITE_TARGET%/}/" && offsite_ok=1
else
warn "rsync не установлен: offsite-копия не сделана"
fi
;;
esac
if [ "$offsite_ok" = "1" ]; then
log "offsite-копия отправлена"
else
warn "offsite-копия не отправлена (локальный бэкап на месте)"
fi
if [ -n "$OFFSITE_KEEP_DAYS" ]; then
case "$OFFSITE_TARGET" in
rclone:*)
if [ "$offsite_ok" = "1" ]; then
rclone delete --min-age "${OFFSITE_KEEP_DAYS}d" "${OFFSITE_TARGET#rclone:}" &&
log "offsite-ротация: удалены копии старше ${OFFSITE_KEEP_DAYS} дн." ||
warn "offsite-ротация не удалась"
fi
;;
*)
warn "offsite-ротация по возрасту поддержана только для rclone — почистите приёмник вручную"
;;
esac
fi
fi
log "бэкапов в каталоге: $(find "$BACKUP_DIR" -maxdepth 1 -name 'glchat-*.tar.gz*' | wc -l)"