99 lines
3.3 KiB
Bash
Executable File
99 lines
3.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
usage() {
|
|
echo "Usage: $0 <frameflow-backup-directory>" >&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}"
|