126 lines
3.8 KiB
Bash
Executable File
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)"
|