Files
tg-vault/deploy/backup.sh
T
hicocos 56276d35fc fix: close post-review durability gaps
Resolve follow-up review blockers around storage account leases, account deletion constraints, restart-safe write reconciliation, and backup maintenance recovery.
2026-07-14 09:44:37 +08:00

89 lines
3.2 KiB
Bash
Executable File
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
set -euo pipefail
if [[ ! -f docker-compose.yml ]]; then
echo "请从包含 docker-compose.yml 的项目目录运行。" >&2
exit 1
fi
PROJECT_ROOT=$(pwd -P)
BACKUP_ROOT=${BACKUP_DIR:-./backups}
STAMP=$(date -u +%Y%m%dT%H%M%SZ)
DEST="$BACKUP_ROOT/$STAMP"
mkdir -p "$DEST"
DB_FILE="$DEST/postgres.dump"
DATA_FILE="$DEST/file-storage.tar.gz"
BACKEND_WAS_RUNNING=false
BACKEND_CONTAINER=$(docker compose ps -q backend 2>/dev/null || true)
VOLUME_NAME=''
if [[ -n "$BACKEND_CONTAINER" ]]; then
VOLUME_NAME=$(docker inspect --format='{{range .Mounts}}{{if eq .Destination "/data"}}{{.Name}}{{end}}{{end}}' "$BACKEND_CONTAINER" 2>/dev/null || true)
BACKEND_WAS_RUNNING=true
fi
VOLUME_NAME=${VOLUME_NAME:-${COMPOSE_PROJECT_NAME:-tg-vault}_file-storage}
# Fail before the maintenance window if the destination cannot hold an uncompressed
# upper bound of the data volume plus the database dump and safety reserve.
VOLUME_BYTES=$(docker run --rm -v "$VOLUME_NAME:/data:ro" alpine:3.20 sh -c 'du -sb /data | cut -f1' | tail -1 | cut -f1 | tr -d ' ')
DATABASE_BYTES=$(docker compose exec -T postgres psql -U tgvault -d tgvault -Atqc 'SELECT pg_database_size(current_database())' | tail -1 | tr -d ' ')
AVAILABLE_BYTES=${BACKUP_AVAILABLE_BYTES_OVERRIDE:-$(df --output=avail -B1 "$DEST" | tail -1 | tr -d ' ')}
MIN_FREE_BYTES=${BACKUP_MIN_FREE_BYTES:-536870912}
for value in "$VOLUME_BYTES" "$DATABASE_BYTES" "$AVAILABLE_BYTES" "$MIN_FREE_BYTES"; do
[[ "$value" =~ ^[0-9]+$ ]] || { rm -rf "$DEST"; echo "无法计算备份容量需求。" >&2; exit 1; }
done
REQUIRED_BYTES=$((VOLUME_BYTES + DATABASE_BYTES + MIN_FREE_BYTES))
if (( AVAILABLE_BYTES < REQUIRED_BYTES )); then
rm -rf "$DEST"
echo "备份目标空间不足:需要至少 ${REQUIRED_BYTES} bytes,可用 ${AVAILABLE_BYTES} bytes;尚未停止 backend。" >&2
exit 1
fi
restart_backend() {
if [[ "$BACKEND_WAS_RUNNING" == true ]]; then
BACKEND_WAS_RUNNING=false
docker compose start backend
fi
}
abort_backup() {
local signal=$1
restart_backend
trap - "$signal"
kill -s "$signal" "$$"
}
trap restart_backend EXIT
trap 'abort_backup INT' INT
trap 'abort_backup TERM' TERM
if [[ "$BACKEND_WAS_RUNNING" == true ]]; then
echo "进入备份维护窗口:停止 backend,阻止上传、删除和后台任务跨越快照边界。"
docker compose stop -t "${BACKUP_BACKEND_STOP_TIMEOUT:-35}" backend
fi
docker compose exec -T postgres pg_dump -U tgvault -d tgvault -Fc > "$DB_FILE"
docker run --rm \
-v "$VOLUME_NAME:/data:ro" \
-v "$(realpath "$DEST"):/backup" \
alpine:3.20 sh -c 'tar -C /data -czf /backup/file-storage.tar.gz .'
(
cd "$DEST"
{
echo "created_at=$STAMP"
echo "git_revision=$(git -C "$PROJECT_ROOT" rev-parse HEAD 2>/dev/null || echo unknown)"
echo "consistency=backend-stopped"
echo "file_storage_volume=$VOLUME_NAME"
echo "compose_services=$(cd "$PROJECT_ROOT" && docker compose config --services | tr '\n' ',')"
sha256sum postgres.dump file-storage.tar.gz
} > manifest.txt
)
chmod -R go-rwx "$DEST"
restart_backend
trap - EXIT INT TERM
echo "备份已创建:$DEST"
echo "一致性策略:backend-stopped(DB dump 与 /data 归档期间无应用写入)。"
echo "该目录包含数据库与密钥材料,请加密并复制到异地存储。"