#!/bin/bash # Выгрузка бэкапа fr1 для rpi. Forced command в ~rus/.ssh/authorized_keys: # restrict,command="/home/rus/bin/backup-export" <ключ server-backup@rpi> # Клиент передаёт профиль: daily | weekly. В stdout — tar | zstd | age (шифрует ОТКРЫТЫМ # ключом, закрытый только у владельца вне малины — rpi хранит то, что сама прочитать не может). # В stderr последней строкой — "SHA256 " шифротекста: rpi сверяет, что дошло целиком. # # Читает root-файлы через docker (rus в группе docker = root-эквивалент), а не через sudo: # sudo на fr1 по паролю. Монтирование / только read-only. set -euo pipefail PUB=age1qfsgzyglv0vmq4kskg888hzgq032mry5kxz9kptkvzsrz0fz0v7q6kxwk2 AGE=/home/rus/bin/age IMG=debian:stable-slim # GNU tar: --exclude/--warning, в alpine — busybox profile="${SSH_ORIGINAL_COMMAND:-daily}" tmp=$(mktemp -d /tmp/backup-export.XXXXXX) cleanup() { rm -rf "$tmp"; docker exec gitea rm -f /tmp/gitea-snap.db >/dev/null 2>&1 || true; } trap cleanup EXIT case "$profile" in daily) # Gitea на SQLite: копировать живой gitea.db нельзя (рваная копия) — .backup даёт согласованный снимок docker exec -u 1000 gitea sqlite3 /data/gitea.db ".backup '/tmp/gitea-snap.db'" docker cp gitea:/tmp/gitea-snap.db "$tmp/gitea.db" >/dev/null paths=(host/etc host/srv/gitea host/usr/local host/home/rus host/var/spool/cron host/root snap) excl=(--exclude=host/srv/gitea/data/gitea.db --exclude='host/srv/gitea/data/gitea.db-*' --exclude=host/home/rus/backups --exclude=host/home/rus/.openclaw --exclude=host/home/rus/openclaw --exclude=host/home/rus/openclaw-work --exclude=host/home/rus/.cache --exclude=host/home/rus/.claude --exclude='*.bak-*' --exclude='*.tar.gz') ;; weekly) paths=(host/var/lib/docker/volumes/claude-channels_claude-home host/var/lib/docker/volumes/monitoring_grafana_data host/var/lib/docker/volumes/monitoring_alertmanager_data host/var/lib/docker/volumes/monitoring_pushgateway_data host/home/tgclaude) excl=(--exclude=host/home/tgclaude/backups --exclude=host/home/tgclaude/openclaw-deploy --exclude=host/home/tgclaude/.local --exclude=host/home/tgclaude/.cache --exclude=host/home/tgclaude/chrome-libs) ;; *) echo "профиль: daily|weekly" >&2; exit 2 ;; esac # tar exit 1 = «файл изменился при чтении» (живые логи/репо) — не ошибка для бэкапа docker run --rm --network none -v /:/host:ro -v "$tmp":/snap:ro "$IMG" \ sh -c 'tar --numeric-owner --ignore-failed-read --warning=no-file-changed -C / -cf - "$@"; rc=$?; [ $rc -le 1 ] || exit $rc' tar "${excl[@]}" "${paths[@]}" \ | zstd -3 -T0 -q | "$AGE" -r "$PUB" | tee >(sha256sum > "$tmp/sum") # процесс-подстановка живёт отдельно от конвейера — ждём, пока sha256sum допишет итог for _ in $(seq 300); do [ -s "$tmp/sum" ] && break; sleep 0.1; done echo "SHA256 $(cut -d' ' -f1 "$tmp/sum")" >&2