#!/usr/bin/env bash
# pg-dump-to-s3.sh: dump one PostgreSQL database, check the archive, upload it
# to S3 or any S3-compatible bucket (R2, B2, Wasabi, MinIO).
# From https://backupdrill.com/guides/pg-dump-to-s3 (MIT licensed).
#
# Needs pg_dump and pg_restore of the same major version as the server, and
# the AWS CLI v2. Configure with environment variables:
#   DATABASE_URL     postgresql://user@host:5432/dbname (password in ~/.pgpass)
#   S3_BUCKET        bucket name
#   S3_PREFIX        folder inside the bucket (default: postgres)
#   AGE_RECIPIENT    optional: age public key; the dump is encrypted before upload
#   AWS_ENDPOINT_URL for anything that is not AWS S3, e.g.
#                    https://<account-id>.r2.cloudflarestorage.com
set -euo pipefail

: "${DATABASE_URL:?DATABASE_URL is not set}"
: "${S3_BUCKET:?S3_BUCKET is not set}"
prefix="${S3_PREFIX:-postgres}"
stamp="$(date -u +%Y-%m-%dT%H-%M-%SZ)"

workdir="$(mktemp -d)"
trap 'rm -rf "$workdir"' EXIT
umask 077
dump="$workdir/$stamp.dump"

# 1. Dump. Custom format is compressed, and pg_restore can restore
#    single tables from it or run in parallel.
pg_dump --format=custom --file="$dump" --dbname="$DATABASE_URL"

# 2. Read the whole archive back before it leaves the machine: --list checks
#    the table of contents, --file=/dev/null decompresses every row. A truncated
#    or corrupt file fails here instead of on the day you need it.
pg_restore --list "$dump" > "$workdir/contents.txt"
pg_restore --file=/dev/null "$dump"
tables="$(grep -c ' TABLE DATA ' "$workdir/contents.txt" || true)"
echo "dump OK: $tables tables with data, $(wc -c < "$dump" | tr -d ' ') bytes"

# 3. Optionally encrypt. Only the holder of the matching private key can
#    read it, so a leaked bucket key does not leak the database.
upload="$dump"
if [ -n "${AGE_RECIPIENT:-}" ]; then
  age --recipient "$AGE_RECIPIENT" --output "$dump.age" "$dump"
  upload="$dump.age"
fi
name="$(basename "$upload")"

# 4. Upload the file, then a checksum next to it.
if command -v sha256sum > /dev/null; then
  sum="$(sha256sum "$upload" | cut -d ' ' -f 1)"
else
  sum="$(shasum -a 256 "$upload" | cut -d ' ' -f 1)"
fi
aws s3 cp "$upload" "s3://$S3_BUCKET/$prefix/$name" --only-show-errors
printf '%s  %s\n' "$sum" "$name" \
  | aws s3 cp - "s3://$S3_BUCKET/$prefix/$name.sha256" --only-show-errors
echo "uploaded s3://$S3_BUCKET/$prefix/$name"
