The watcher scripts now write live status and append-only run history onto the shared volume; the api exposes them via /shopdbbackups/status and /history. The restore modal shows an indeterminate spinner with phase labels, and two cards list backup and restore run records. Timestamps render in UTC to match the Zulu-stamped set ids, the history directory is excluded from the set list, and a stale-status guard stops a previous run's done phase from collapsing the spinner on the next restore.
62 lines
2.5 KiB
Bash
62 lines
2.5 KiB
Bash
#!/usr/bin/env bash
|
|
# ShopDB backup WATCHER - the resident half of the in-app backup utility.
|
|
#
|
|
# WHY this exists: the api container cannot run docker, so when an operator
|
|
# clicks "Back up now" the api only drops a sentinel file (.backup-requested)
|
|
# on the shared backups volume. This watcher is the thing that actuates that
|
|
# request: it polls for the sentinel and runs the one-shot backup.sh, which
|
|
# ships a restic snapshot to the backup-store container and clears the sentinel
|
|
# on success. That turns the UI button into a real, self-serve backup without
|
|
# giving the api container any docker or host privileges.
|
|
#
|
|
# ASCII only, no emojis - matches the repo style gate.
|
|
set -uo pipefail
|
|
|
|
BACKUPS_ROOT="${SHOPDB_BACKUPS_ROOT:-/backups}"
|
|
SENTINEL="${BACKUPS_ROOT}/.backup-requested"
|
|
RESTORE_SENTINEL="${BACKUPS_ROOT}/.restore-requested"
|
|
INTERVAL="${BACKUP_POLL_SECONDS:-10}"
|
|
|
|
log() { echo "[backup-watch] $*"; }
|
|
|
|
# shellcheck source=scripts/backup/status.sh
|
|
. /opt/backup/status.sh
|
|
|
|
mkdir -p "${BACKUPS_ROOT}"
|
|
# The api container runs unprivileged and only drops sentinel files here; it
|
|
# cannot write into a root-owned 0755 directory. Make the backups root
|
|
# writable so "Back up now" and "Restore" can queue their sentinels.
|
|
chmod 0777 "${BACKUPS_ROOT}" 2>/dev/null || true
|
|
log "watcher up; polling ${SENTINEL} and ${RESTORE_SENTINEL} every ${INTERVAL}s"
|
|
|
|
while true; do
|
|
if [ -f "${SENTINEL}" ]; then
|
|
log "backup requested - running one-shot"
|
|
status_write backup queued "" "Backup queued"
|
|
if /opt/backup/backup.sh; then
|
|
log "backup completed"
|
|
else
|
|
# backup.sh only clears the sentinel on success, so a failed run
|
|
# would otherwise loop forever. Clear it here and wait for the next
|
|
# explicit request rather than hammering a broken backup.
|
|
log "backup FAILED - clearing request, awaiting next"
|
|
rm -f "${SENTINEL}" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
if [ -f "${RESTORE_SENTINEL}" ]; then
|
|
# The api drops the target set name into the restore sentinel. Read it,
|
|
# hand it to restore.sh, and clear the request whether it succeeds or
|
|
# fails so a bad set cannot pin the watcher in a tight retry loop.
|
|
setname="$(head -n1 "${RESTORE_SENTINEL}" 2>/dev/null | tr -d '\r\n')"
|
|
log "restore requested - running restore for ${setname}"
|
|
status_write restore queued "${setname}" "Restore queued"
|
|
if /opt/backup/restore.sh "${setname}"; then
|
|
log "restore completed"
|
|
else
|
|
log "restore FAILED - clearing request, awaiting next"
|
|
fi
|
|
rm -f "${RESTORE_SENTINEL}" 2>/dev/null || true
|
|
fi
|
|
sleep "${INTERVAL}"
|
|
done
|