#!/usr/bin/env bash set -Eeuo pipefail usage() { echo "Usage: $0 " >&2 exit 2 } fail() { echo "Backup verification failed: $1" >&2 exit 1 } [[ $# -eq 1 ]] || usage [[ -d "$1" ]] || fail "directory not found: $1" [[ ! -L "$1" ]] || fail "backup directory must not be a symbolic link" backup_dir="$(cd "$1" && pwd -P)" required_files=(postgres.dump redis-data.tar.gz minio-data.tar.gz manifest.txt SHA256SUMS) for filename in "${required_files[@]}"; do path="${backup_dir}/${filename}" [[ -f "${path}" ]] || fail "missing ${filename}" [[ ! -L "${path}" ]] || fail "${filename} must not be a symbolic link" [[ -s "${path}" ]] || fail "${filename} is empty" done created_at="$(sed -n 's/^created_at=//p' "${backup_dir}/manifest.txt")" compose_project="$(sed -n 's/^compose_project=//p' "${backup_dir}/manifest.txt")" [[ "${created_at}" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || fail "manifest has an invalid created_at value" [[ "${compose_project}" =~ ^[a-zA-Z0-9][a-zA-Z0-9_.-]*$ ]] || fail "manifest has an invalid compose_project value" postgres_seen=0 redis_seen=0 minio_seen=0 checksum_rows=0 while read -r digest filename extra; do [[ -z "${extra:-}" ]] || fail "SHA256SUMS contains an invalid row" [[ "${digest}" =~ ^[[:xdigit:]]{64}$ ]] || fail "SHA256SUMS contains an invalid digest" filename="${filename#\*}" case "${filename}" in postgres.dump) postgres_seen=$((postgres_seen + 1)) ;; redis-data.tar.gz) redis_seen=$((redis_seen + 1)) ;; minio-data.tar.gz) minio_seen=$((minio_seen + 1)) ;; *) fail "SHA256SUMS references an unexpected file: ${filename}" ;; esac checksum_rows=$((checksum_rows + 1)) done < "${backup_dir}/SHA256SUMS" [[ ${checksum_rows} -eq 3 && ${postgres_seen} -eq 1 && ${redis_seen} -eq 1 && ${minio_seen} -eq 1 ]] \ || fail "SHA256SUMS must reference each data artifact exactly once" if command -v sha256sum >/dev/null 2>&1; then (cd "${backup_dir}" && sha256sum -c SHA256SUMS) >/dev/null \ || fail "artifact checksum mismatch" else (cd "${backup_dir}" && shasum -a 256 -c SHA256SUMS) >/dev/null \ || fail "artifact checksum mismatch" fi postgres_magic="$(LC_ALL=C head -c 5 "${backup_dir}/postgres.dump")" [[ "${postgres_magic}" == "PGDMP" ]] || fail "PostgreSQL dump does not use the required custom format" verify_archive_paths() { local archive_name="$1" local listing_file local entry local normalized listing_file="$(mktemp "${TMPDIR:-/tmp}/frameflow-archive-list.XXXXXX")" if ! tar -tzf "${backup_dir}/${archive_name}" > "${listing_file}"; then rm -f "${listing_file}" fail "${archive_name} is not a readable gzip tar archive" fi if [[ ! -s "${listing_file}" ]]; then rm -f "${listing_file}" fail "${archive_name} contains no entries" fi while IFS= read -r entry; do [[ "${entry}" != /* ]] || { rm -f "${listing_file}" fail "${archive_name} contains an absolute path" } normalized="${entry#./}" case "/${normalized}/" in *"/../"*) rm -f "${listing_file}" fail "${archive_name} contains a parent-directory path" ;; esac done < "${listing_file}" rm -f "${listing_file}" } verify_archive_paths redis-data.tar.gz verify_archive_paths minio-data.tar.gz echo "Backup verification passed: ${backup_dir}" echo "Source Compose project: ${compose_project}; created at: ${created_at}"