Files
tilbudgivern/scripts/backup-root-recovery.sh
T

126 lines
3.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Keep one compact disaster-recovery set on the root filesystem.
# Application code is recovered from GitHub; this set only contains the
# encrypted runtime environment and the newest logical MariaDB backup.
set -euo pipefail
umask 077
PROJECT_ROOT="/mnt/HC_Volume_103713257/tilbudgivern"
DATA_BACKUP_DIR="$PROJECT_ROOT/backups"
RECOVERY_ROOT="/home/alex/tilbudgivern-recovery"
KEY_DIR="/home/alex/.config/tilbudgivern"
KEY_FILE="$KEY_DIR/recovery-backup.key"
LOCK_FILE="$RECOVERY_ROOT/.backup.lock"
MAX_DATABASE_BYTES=$((750 * 1024 * 1024))
MIN_FREE_BYTES=$((2 * 1024 * 1024 * 1024))
log() {
printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$1"
}
require_command() {
if ! command -v "$1" >/dev/null 2>&1; then
log "ERROR: Required command not found: $1"
exit 1
fi
}
require_command flock
require_command openssl
require_command sha256sum
install -d -m 0700 "$RECOVERY_ROOT" "$KEY_DIR"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
log "Another root recovery backup is already running"
exit 0
fi
if [ ! -s "$KEY_FILE" ]; then
key_temp="$KEY_FILE.tmp"
openssl rand 32 > "$key_temp"
chmod 0600 "$key_temp"
mv "$key_temp" "$KEY_FILE"
log "Created recovery encryption key: $KEY_FILE"
fi
latest_database="$({
find -L "$DATA_BACKUP_DIR" -maxdepth 1 -type f \
-name 'mariadb_all_databases_*.sql.gz' -printf '%T@ %p\n'
} | sort -nr | head -n 1 | cut -d' ' -f2-)"
if [ -z "$latest_database" ] || [ ! -s "$latest_database" ]; then
log "ERROR: No MariaDB backup found in $DATA_BACKUP_DIR"
exit 1
fi
database_bytes="$(stat -c '%s' "$latest_database")"
if [ "$database_bytes" -gt "$MAX_DATABASE_BYTES" ]; then
log "ERROR: Database backup exceeds the 750 MiB root recovery limit"
exit 1
fi
available_bytes="$(df --output=avail -B1 "$RECOVERY_ROOT" | tail -n 1 | tr -d ' ')"
required_bytes=$((database_bytes + MIN_FREE_BYTES))
if [ "$available_bytes" -lt "$required_bytes" ]; then
log "ERROR: Root filesystem would have less than 2 GiB free after backup"
exit 1
fi
staging_dir="$(mktemp -d "$RECOVERY_ROOT/.staging.XXXXXX")"
previous_dir="$RECOVERY_ROOT/.previous"
cleanup() {
if [ -n "${staging_dir:-}" ] && [ -d "$staging_dir" ]; then
rm -rf -- "$staging_dir"
fi
}
trap cleanup EXIT
install -d -m 0700 "$staging_dir/config" "$staging_dir/database"
openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt \
-in "$PROJECT_ROOT/backend/.env" \
-out "$staging_dir/config/backend.env.enc" \
-pass "file:$KEY_FILE"
cp -- "$PROJECT_ROOT/backend/.env.example" "$staging_dir/config/backend.env.example"
cp -- "$latest_database" "$staging_dir/database/$(basename "$latest_database")"
cat > "$staging_dir/RESTORE.txt" <<'EOF'
Code: clone the Git repository and check out the wanted commit.
Configuration:
openssl enc -d -aes-256-cbc -pbkdf2 -iter 200000 \
-in config/backend.env.enc -out backend/.env \
-pass file:/home/alex/.config/tilbudgivern/recovery-backup.key
Database:
gzip -dc database/mariadb_all_databases_*.sql.gz | mysql -u root -p
EOF
printf 'created_at=%s\n' "$(date --iso-8601=seconds)" > "$staging_dir/MANIFEST"
printf 'source_commit=%s\n' "$(git -C "$PROJECT_ROOT" rev-parse HEAD)" >> "$staging_dir/MANIFEST"
printf 'database_file=%s\n' "$(basename "$latest_database")" >> "$staging_dir/MANIFEST"
printf 'database_bytes=%s\n' "$database_bytes" >> "$staging_dir/MANIFEST"
(
cd "$staging_dir"
sha256sum MANIFEST RESTORE.txt config/backend.env.enc \
config/backend.env.example database/*.sql.gz > SHA256SUMS
sha256sum --check SHA256SUMS >/dev/null
)
rm -rf -- "$previous_dir"
if [ -d "$RECOVERY_ROOT/current" ]; then
mv "$RECOVERY_ROOT/current" "$previous_dir"
fi
mv "$staging_dir" "$RECOVERY_ROOT/current"
staging_dir=""
rm -rf -- "$previous_dir"
recovery_size="$(du -sh "$RECOVERY_ROOT/current" | cut -f1)"
log "Root recovery backup ready: $RECOVERY_ROOT/current ($recovery_size)"