#!/usr/bin/env bash set -Eeuo pipefail usage() { cat >&2 <<'EOF' Usage: scheduled-maintenance.sh Requires FRAMEFLOW_BACKUP_MOUNTPOINT and FRAMEFLOW_BACKUP_DESTINATION. The destination must be a writable directory inside the active mount point. EOF exit "${1:-2}" } fail() { echo "Scheduled maintenance failed: $1" >&2 exit 1 } [[ $# -eq 1 ]] || usage case "$1" in backup|restore-latest) mode="$1" ;; --help|-h) usage 0 ;; *) usage ;; esac script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" mount_point="${FRAMEFLOW_BACKUP_MOUNTPOINT:-}" backup_destination="${FRAMEFLOW_BACKUP_DESTINATION:-}" [[ -n "${mount_point}" ]] || fail "FRAMEFLOW_BACKUP_MOUNTPOINT is required" [[ -n "${backup_destination}" ]] || fail "FRAMEFLOW_BACKUP_DESTINATION is required" for path in "${mount_point}" "${backup_destination}"; do [[ "${path}" == /* ]] || fail "backup paths must be absolute: ${path}" [[ "${path}" != "/" ]] || fail "the filesystem root cannot be used for backups" [[ ! "${path}" =~ [[:space:]] ]] || fail "backup paths must not contain whitespace: ${path}" [[ -d "${path}" ]] || fail "backup directory does not exist: ${path}" [[ ! -L "${path}" ]] || fail "backup paths must not be symbolic links: ${path}" done mount_point="$(cd "${mount_point}" && pwd -P)" backup_destination="$(cd "${backup_destination}" && pwd -P)" if [[ "${backup_destination}" != "${mount_point}" && "${backup_destination}" != "${mount_point}/"* ]]; then fail "backup destination must be inside FRAMEFLOW_BACKUP_MOUNTPOINT" fi [[ -w "${backup_destination}" ]] || fail "backup destination is not writable: ${backup_destination}" command -v findmnt >/dev/null 2>&1 || fail "findmnt is required to verify the backup mount" mounted_target="$(findmnt -rn -M "${mount_point}" -o TARGET 2>/dev/null)" \ || fail "backup mount is not active: ${mount_point}" [[ "${mounted_target}" == "${mount_point}" ]] \ || fail "backup mount resolved to an unexpected target: ${mounted_target}" case "${mode}" in backup) exec "${script_dir}/backup.sh" "${backup_destination}" ;; restore-latest) latest_name="" latest_path="" while IFS= read -r -d '' candidate; do [[ ! -L "${candidate}" ]] || continue candidate_name="${candidate##*/}" [[ "${candidate_name}" =~ ^frameflow-[0-9]{8}T[0-9]{6}Z$ ]] || continue if [[ -z "${latest_name}" || "${candidate_name}" > "${latest_name}" ]]; then latest_name="${candidate_name}" latest_path="${candidate}" fi done < <(find "${backup_destination}" -mindepth 1 -maxdepth 1 -type d -name 'frameflow-*' -print0) [[ -n "${latest_path}" ]] || fail "no timestamped FrameFlow backup was found" echo "Selected latest backup for restore rehearsal: ${latest_path}" "${script_dir}/verify-backup.sh" "${latest_path}" exec "${script_dir}/rehearse-restore.sh" "${latest_path}" ;; esac