Wire restore into the backup watcher and fix backup timestamps
The api queued .restore-requested but nothing consumed it, so restores silently did nothing. The watcher now services the restore sentinel by running restore.sh, and its instance_data mount is read-write so restore can lay files back down (the manual one-shot stays read-only). Backup stamps use a colon-free UTC form that Date could not parse, so the UI showed raw strings; normalize them before formatting.
This commit is contained in:
@@ -110,9 +110,11 @@ services:
|
|||||||
SHOPDB_BACKUPS_ROOT: /backups
|
SHOPDB_BACKUPS_ROOT: /backups
|
||||||
BACKUP_POLL_SECONDS: ${BACKUP_POLL_SECONDS:-10}
|
BACKUP_POLL_SECONDS: ${BACKUP_POLL_SECONDS:-10}
|
||||||
volumes:
|
volumes:
|
||||||
# The files half of the backup scope, read-only: we never mutate the live
|
# The watcher services both backup (read) and restore (write) requests, so
|
||||||
# instance tree from the backup path.
|
# the instance tree is mounted read-write here. Restore has to lay the
|
||||||
- instance_data:/app/instance:ro
|
# files half of a set back down next to the DB rows that reference it; the
|
||||||
|
# backup half still only ever reads. The manual one-shot below stays :ro.
|
||||||
|
- instance_data:/app/instance
|
||||||
# Alembic chain, read-only, so the manifest can stamp the schema revision
|
# Alembic chain, read-only, so the manifest can stamp the schema revision
|
||||||
# the running code is at.
|
# the running code is at.
|
||||||
- ./migrations:/app/migrations:ro
|
- ./migrations:/app/migrations:ro
|
||||||
|
|||||||
@@ -190,7 +190,7 @@
|
|||||||
<div v-if="restoreTarget" class="modal-backdrop" @click.self="closeRestore">
|
<div v-if="restoreTarget" class="modal-backdrop" @click.self="closeRestore">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<h3>Restore this backup?</h3>
|
<h3>Restore this backup?</h3>
|
||||||
<p class="modal-set">{{ restoreTarget.snapshotid }}</p>
|
<p class="modal-set">{{ formatTaken(restoreTarget.takenat || restoreTarget.snapshotid) }}</p>
|
||||||
|
|
||||||
<div class="compat" :class="restoreTarget.iscompatible ? 'ok' : 'warn'">
|
<div class="compat" :class="restoreTarget.iscompatible ? 'ok' : 'warn'">
|
||||||
<component :is="restoreTarget.iscompatible ? ShieldCheck : AlertTriangle" :size="18" />
|
<component :is="restoreTarget.iscompatible ? ShieldCheck : AlertTriangle" :size="18" />
|
||||||
@@ -356,7 +356,7 @@ const STALE_HOURS = 48
|
|||||||
|
|
||||||
const hoursSinceLastGood = computed(() => {
|
const hoursSinceLastGood = computed(() => {
|
||||||
if (!lastGoodAt.value) return null
|
if (!lastGoodAt.value) return null
|
||||||
const then = new Date(lastGoodAt.value.replace(/Z$/, 'Z')).getTime()
|
const then = new Date(normalizeStamp(lastGoodAt.value)).getTime()
|
||||||
return (Date.now() - then) / 36e5
|
return (Date.now() - then) / 36e5
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -383,11 +383,21 @@ const bannerSub = computed(() => {
|
|||||||
// -- Formatting -------------------------------------------------------------
|
// -- Formatting -------------------------------------------------------------
|
||||||
function formatTaken(value) {
|
function formatTaken(value) {
|
||||||
if (!value) return 'unknown'
|
if (!value) return 'unknown'
|
||||||
const date = new Date(value.replace(/Z$/, 'Z'))
|
const date = new Date(normalizeStamp(value))
|
||||||
if (Number.isNaN(date.getTime())) return value
|
if (Number.isNaN(date.getTime())) return value
|
||||||
return date.toLocaleString()
|
return date.toLocaleString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backup ids and manifest stamps use a filename-safe UTC form with no colons
|
||||||
|
// (YYYY-MM-DDTHHMMSSZ, sometimes prefixed with a site name), which the Date
|
||||||
|
// parser rejects. Reinsert the colons so it parses as real ISO 8601.
|
||||||
|
function normalizeStamp(value) {
|
||||||
|
const match = String(value).match(/(\d{4}-\d{2}-\d{2})T(\d{2})(\d{2})(\d{2})Z$/)
|
||||||
|
if (!match) return value
|
||||||
|
const [, day, hour, minute, second] = match
|
||||||
|
return `${day}T${hour}:${minute}:${second}Z`
|
||||||
|
}
|
||||||
|
|
||||||
function formatSize(bytes) {
|
function formatSize(bytes) {
|
||||||
if (!bytes) return '0 B'
|
if (!bytes) return '0 B'
|
||||||
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
const units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
|||||||
@@ -14,12 +14,17 @@ set -uo pipefail
|
|||||||
|
|
||||||
BACKUPS_ROOT="${SHOPDB_BACKUPS_ROOT:-/backups}"
|
BACKUPS_ROOT="${SHOPDB_BACKUPS_ROOT:-/backups}"
|
||||||
SENTINEL="${BACKUPS_ROOT}/.backup-requested"
|
SENTINEL="${BACKUPS_ROOT}/.backup-requested"
|
||||||
|
RESTORE_SENTINEL="${BACKUPS_ROOT}/.restore-requested"
|
||||||
INTERVAL="${BACKUP_POLL_SECONDS:-10}"
|
INTERVAL="${BACKUP_POLL_SECONDS:-10}"
|
||||||
|
|
||||||
log() { echo "[backup-watch] $*"; }
|
log() { echo "[backup-watch] $*"; }
|
||||||
|
|
||||||
mkdir -p "${BACKUPS_ROOT}"
|
mkdir -p "${BACKUPS_ROOT}"
|
||||||
log "watcher up; polling ${SENTINEL} every ${INTERVAL}s"
|
# 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
|
while true; do
|
||||||
if [ -f "${SENTINEL}" ]; then
|
if [ -f "${SENTINEL}" ]; then
|
||||||
@@ -34,5 +39,18 @@ while true; do
|
|||||||
rm -f "${SENTINEL}" 2>/dev/null || true
|
rm -f "${SENTINEL}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
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}"
|
||||||
|
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}"
|
sleep "${INTERVAL}"
|
||||||
done
|
done
|
||||||
|
|||||||
Reference in New Issue
Block a user