refactor: retire the images tarball backup
CI and images / lint (push) Successful in 2s
CI and images / extension-version (push) Successful in 2s
CI and images / extension-test (push) Successful in 20s
CI and images / frontend-build (push) Successful in 25s
CI and images / backend-lint-and-test (push) Successful in 50s
CI and images / integration (push) Successful in 2m38s
CI and images / build-agent (push) Successful in 5s
CI and images / sign-extension (push) Successful in 8s
CI and images / build-web (push) Successful in 4m10s
CI and images / smoke-web (push) Successful in 1m3s
CI and images / promote (push) Successful in 1s
CI and images / refresh-dev (push) Skipped
CI and images / unattended-failure (push) Skipped

Images and attachments are backed up off-site, one object per file, so the
tar+zstd of /images goes: its service and restore, both tasks, the trigger and
restore routes, the Keep last setting, the screen's Images section, and the
tests. Migration 0122 drops backup_run.tar_path and
import_settings.backup_images_keep_last_n and deletes any images runs.

The local database dump stays for an install with no off-site destination,
and the Backups card now says plainly that images and attachments are backed
up only off-site.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-07 22:07:44 -05:00
co-authored by Claude Opus 5.5
parent 794b04a64e
commit 14936b62dc
14 changed files with 92 additions and 393 deletions
@@ -58,7 +58,7 @@ import { computed, ref, watch } from 'vue'
const props = defineProps({
modelValue: { type: Boolean, required: true },
action: { type: String, required: true }, // 'restore' | 'delete'
kind: { type: String, required: true }, // 'db' | 'images' | 'artist' | 'tag' | 'images-selection' | 'audit' | 'min-dim'
kind: { type: String, required: true }, // 'db' | 'offsite' | 'artist' | 'tag' | 'images-selection' | 'audit' | 'min-dim'
runId: { type: [Number, String], default: '' }, // numeric id or sha8 string
description: { type: String, default: '' },
tier: { type: String, default: 'C' }, // 'B' | 'C'
@@ -2,7 +2,7 @@
<MaintenanceTile
icon="mdi-database-export"
title="Backups"
blurb="Database + image backups, schedules and restore."
blurb="Database and library backups, schedules and restore."
>
<p v-if="offsite" class="fc-muted text-body-2 mb-4">
Database dumps stream to the <strong>{{ offsite.bucket }}</strong> bucket
@@ -11,11 +11,10 @@
bucket's <code>media/</code> folder, below.
</p>
<p v-else class="fc-muted text-body-2 mb-4">
pg_dump for the database (fast, nightly-schedulable);
tar+zstd for the images (slow, manual only). Files live in
<code>/images/_backups/</code>. Tag a backup to protect it
from autoprune. Set up an off-site destination above to send
database dumps off this server instead.
Database dumps are written to <code>/images/_backups/</code> on this
server; tag one to keep it past Keep last. Images and attachments are
backed up only to an off-site destination: set one up above to back up
the whole library, database included, off this server.
</p>
<v-alert
@@ -171,34 +170,6 @@
/>
</template>
<template v-else>
<!-- Images section -->
<h3 class="fc-section-title">Images</h3>
<div class="fc-settings-row mb-3">
<v-text-field
:model-value="settings.backup_images_keep_last_n"
label="Keep last" type="number" min="1" max="100"
density="compact" hide-details style="max-width: 110px;"
@update:model-value="onSettingChange('backup_images_keep_last_n', Number($event))"
/>
</div>
<v-btn
color="accent" variant="flat" rounded="pill"
prepend-icon="mdi-folder-zip" class="mb-3"
:loading="imagesTriggering"
@click="onTrigger('images')"
>Run images backup now</v-btn>
<span class="text-caption fc-muted ml-2">
ⓘ Hours for a 200GB+ library.
</span>
<BackupRunsTable
:runs="store.imagesRuns"
@restore="onRestore"
@delete="onDelete"
@tag="onTag"
/>
</template>
<BackupConfirmModal
v-model="confirmOpen"
:action="confirmAction"
@@ -248,7 +219,6 @@ const settings = computed(() => store.settings || {
backup_db_nightly_enabled: false,
backup_db_nightly_hour_utc: 3,
backup_db_keep_last_n: 14,
backup_images_keep_last_n: 3,
})
// The off-site destination, once it is fully set up: database dumps go there.
@@ -267,7 +237,6 @@ const runningSent = computed(() => runningMedia.value?.manifest?.sent || 0)
const mediaTriggering = ref(false)
const dbTriggering = ref(false)
const imagesTriggering = ref(false)
const confirmOpen = ref(false)
const confirmAction = ref('restore')
const confirmKind = ref('db')
@@ -356,8 +325,6 @@ function pollOnce() {
store.loadMediaCoverage()
store.loadRuns('reconcile')
}
} else {
store.loadRuns('images')
}
}
@@ -373,7 +340,7 @@ onUnmounted(() => {
})
async function onTrigger(kind) {
const triggeringRef = { db: dbTriggering, images: imagesTriggering, media: mediaTriggering }[kind]
const triggeringRef = { db: dbTriggering, media: mediaTriggering }[kind]
triggeringRef.value = true
try {
await store.triggerBackup(kind)
@@ -395,7 +362,7 @@ function _openConfirm(action, run) {
confirmToken.value = ''
confirmDescription.value = (
action === 'restore'
? `Source artifact: ${run.kind === 'db' ? run.sql_path : run.tar_path}`
? `Source artifact: ${run.sql_path}`
: 'Removes the BackupRun row and unlinks the artifact files.'
)
confirmOpen.value = true
+3 -4
View File
@@ -7,7 +7,6 @@ export const useBackupStore = defineStore('backup', () => {
const api = useApi()
const dbRuns = ref([])
const imagesRuns = ref([])
const mediaRuns = ref([])
const restoreRuns = ref([])
// The weekly check of the bucket against what was sent.
@@ -16,12 +15,12 @@ export const useBackupStore = defineStore('backup', () => {
const testRuns = ref([])
const settings = ref(null)
const loading = ref({
dbRuns: false, imagesRuns: false, mediaRuns: false, restoreRuns: false,
dbRuns: false, mediaRuns: false, restoreRuns: false,
reconcileRuns: false, testRuns: false, settings: false,
})
const lastError = ref(null)
const RUNS = {
db: dbRuns, images: imagesRuns, media: mediaRuns, restore: restoreRuns,
db: dbRuns, media: mediaRuns, restore: restoreRuns,
reconcile: reconcileRuns, test: testRuns,
}
@@ -200,7 +199,7 @@ export const useBackupStore = defineStore('backup', () => {
}
return {
dbRuns, imagesRuns, mediaRuns, restoreRuns, reconcileRuns, testRuns,
dbRuns, mediaRuns, restoreRuns, reconcileRuns, testRuns,
mediaCoverage, loadMediaCoverage,
listOffsiteDumps, databaseBytes, restoreFromOffsite, fetchMissingFiles, testDump,
settings, loading, lastError,