diff --git a/scripts/backup/container-entrypoint.sh b/scripts/backup/container-entrypoint.sh new file mode 100644 index 0000000..b6571a1 --- /dev/null +++ b/scripts/backup/container-entrypoint.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +readonly BACKUP_ROOT="/backup" +readonly STAGING_ROOT="/staging" +readonly UPLOADS_PATH="/data/uploads" +readonly TDLIB_WORKER_PATH="/data/tdlib-worker" +readonly TDLIB_BOT_PATH="/data/tdlib-bot" + +RUN_DIR="" + +require_value() { + local name="$1" + + if [[ -z "${!name:-}" ]]; then + printf 'Required environment variable %s is not set.\n' "$name" >&2 + return 1 + fi +} + +require_directory() { + local path="$1" + + if [[ ! -d "$path" ]]; then + printf 'Required mounted directory %s is unavailable.\n' "$path" >&2 + return 1 + fi +} + +json_escape() { + local input="$1" + local output="" + local character + local code + + while [[ -n "$input" ]]; do + character="${input:0:1}" + input="${input:1}" + + case "$character" in + '"') output+='\"' ;; + '\\') output+='\\' ;; + $'\b') output+='\b' ;; + $'\f') output+='\f' ;; + $'\n') output+='\n' ;; + $'\r') output+='\r' ;; + $'\t') output+='\t' ;; + *) + printf -v code '%d' "'$character" + if (( code < 32 )); then + printf -v character '\u%04x' "$code" + fi + output+="$character" + ;; + esac + done + + printf '%s' "$output" +} + +cleanup() { + local exit_code=$? + + if [[ -n "$RUN_DIR" && "$RUN_DIR" == "$STAGING_ROOT"/* ]]; then + rm -rf -- "$RUN_DIR" + fi + + exit "$exit_code" +} + +run_backup() { + local timestamp + local checksum + local app_version + + require_value DATABASE_URL + require_value RESTIC_REPOSITORY + require_value RESTIC_PASSWORD_FILE + require_value BACKUP_RETENTION_DAYS + + if [[ ! -r "$RESTIC_PASSWORD_FILE" ]]; then + printf 'Restic password file %s is not readable.\n' "$RESTIC_PASSWORD_FILE" >&2 + return 1 + fi + + require_directory "$BACKUP_ROOT" + require_directory "$STAGING_ROOT" + require_directory "$UPLOADS_PATH" + require_directory "$TDLIB_WORKER_PATH" + require_directory "$TDLIB_BOT_PATH" + + timestamp="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" + RUN_DIR="$STAGING_ROOT/backup-${timestamp//[:]/}-$$" + mkdir -- "$RUN_DIR" + mkdir -- "$RUN_DIR/manifest" + trap cleanup EXIT + + pg_dump --format=custom --file="$RUN_DIR/database.dump" "$DATABASE_URL" + + if [[ ! -f "$RUN_DIR/database.dump" || ! -s "$RUN_DIR/database.dump" ]]; then + printf 'PostgreSQL dump is missing or empty.\n' >&2 + return 1 + fi + + sha256sum "$RUN_DIR/database.dump" > "$RUN_DIR/manifest/database.dump.sha256" + checksum="$(awk '{print $1}' "$RUN_DIR/manifest/database.dump.sha256")" + app_version="${BACKUP_APP_VERSION:-unknown}" + + cat > "$RUN_DIR/manifest/backup-manifest.json" <