Release v1.0.0
This commit is contained in:
98
scripts/verify-backup.sh
Executable file
98
scripts/verify-backup.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/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}"
|
||||
Reference in New Issue
Block a user