From adf884c2c4b3a8edfde8f81063a5fe09c17e2102 Mon Sep 17 00:00:00 2001 From: Girish Ramakrishnan Date: Sun, 15 Feb 2026 14:59:27 +0100 Subject: [PATCH] add integrity check to system backups --- dashboard/src/components/BackupInfoDialog.vue | 2 +- dashboard/src/components/SystemBackupList.vue | 56 ++++++++++++++++++- dashboard/src/components/app/Backups.vue | 56 ++++++++----------- 3 files changed, 80 insertions(+), 34 deletions(-) diff --git a/dashboard/src/components/BackupInfoDialog.vue b/dashboard/src/components/BackupInfoDialog.vue index 541aecca6..ad639b5de 100644 --- a/dashboard/src/components/BackupInfoDialog.vue +++ b/dashboard/src/components/BackupInfoDialog.vue @@ -137,7 +137,7 @@ defineExpose({
{{ $t('backups.backupDetails.lastIntegrityCheck') }}
- {{ $t('backups.backupDetails.integrityInProgress') }} + {{ $t('backups.backupDetails.integrityInProgress') }} {{ prettyLongDate(backup.lastIntegrityCheckTime) }} {{ $t('backups.backupDetails.integrityNever') }}
diff --git a/dashboard/src/components/SystemBackupList.vue b/dashboard/src/components/SystemBackupList.vue index 750c8b253..e9d132a40 100644 --- a/dashboard/src/components/SystemBackupList.vue +++ b/dashboard/src/components/SystemBackupList.vue @@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'; const i18n = useI18n(); const t = i18n.t; -import { ref, onMounted, useTemplateRef } from 'vue'; +import { ref, onMounted, onUnmounted, useTemplateRef } from 'vue'; import { Button, FormGroup, TextInput, Checkbox, TableView, Dialog } from '@cloudron/pankow'; import { prettyLongDate, prettyFileSize } from '@cloudron/pankow/utils'; import { TASK_TYPES } from '../constants.js'; @@ -50,6 +50,11 @@ const columns = { sort: true, hideMobile: true, }, + integrity: { + label: 'Integrity', + sort: false, + width: '100px', + }, actions: {} }; @@ -66,6 +71,12 @@ function createActionMenu(backup) { icon: 'fa-solid fa-file-alt', label: t('backups.listing.tooltipDownloadBackupConfig'), action: onDownloadConfig.bind(null, backup), + }, { + separator: true, + }, { + icon: 'fa-solid fa-key', + label: backup.integrityCheckTask?.active ? t('backups.stopIntegrity') : t('backups.checkIntegrity'), + action: backup.integrityCheckTask?.active ? onStopIntegrityCheck.bind(null, backup) : onStartIntegrityCheck.bind(null, backup), }]; } @@ -151,6 +162,20 @@ async function refreshTasks() { }); } +const INTEGRITY_POLL_INTERVAL_MS = 5000; +let integrityPollTimer = null; + +function scheduleIntegrityPoll() { + if (integrityPollTimer) return; + integrityPollTimer = setTimeout(async () => { + integrityPollTimer = null; + await refreshBackups(); + if (backups.value.some(b => b.integrityCheckTask?.active)) { + scheduleIntegrityPoll(); + } + }, INTEGRITY_POLL_INTERVAL_MS); +} + async function refreshBackups() { const [error, result] = await backupsModel.list(); if (error) return console.error(error); @@ -161,6 +186,22 @@ async function refreshBackups() { }); backups.value = result; + + if (result.some(b => b.integrityCheckTask?.active)) { + scheduleIntegrityPoll(); + } +} + +async function onStartIntegrityCheck(backup) { + const [error] = await backupsModel.startIntegrityCheck(backup.id); + if (error) return window.cloudron.onError(error); + await refreshBackups(); +} + +async function onStopIntegrityCheck(backup) { + const [error] = await backupsModel.stopIntegrityCheck(backup.id); + if (error) return window.cloudron.onError(error); + await refreshBackups(); } async function refreshBackupSites() { @@ -231,6 +272,10 @@ onMounted(async () => { await refreshTasks(); }); +onUnmounted(() => { + if (integrityPollTimer) clearTimeout(integrityPollTimer); +}); + defineExpose({ refresh }); @@ -291,6 +336,15 @@ defineExpose({ refresh }); + + diff --git a/dashboard/src/components/app/Backups.vue b/dashboard/src/components/app/Backups.vue index 589cd4d1d..dadbd1fa9 100644 --- a/dashboard/src/components/app/Backups.vue +++ b/dashboard/src/components/app/Backups.vue @@ -103,9 +103,9 @@ function createActionMenu(backup) { separator: true, }, { icon: 'fa-solid fa-key', - label: backup.integrityCheckTaskId ? t('backups.stopIntegrity') : t('backups.checkIntegrity'), + label: backup.integrityCheckTask?.active ? t('backups.stopIntegrity') : t('backups.checkIntegrity'), visible: accessLevel === 'admin', - action: backup.integrityCheckTaskId ? onStopIntegrityCheck.bind(null, backup) : onStartIntegrityCheck.bind(null, backup), + action: backup.integrityCheckTask?.active ? onStopIntegrityCheck.bind(null, backup) : onStartIntegrityCheck.bind(null, backup), }]; } @@ -242,40 +242,16 @@ async function onRestore(backup) { restoreDialog.value.open(); } -const integrityTasks = ref({}); -let integrityPollTimer = null; - -async function refreshIntegrityTasks() { - for (const taskId of Object.keys(integrityTasks.value)) { - const [error, result] = await tasksModel.get(taskId); - if (error) continue; - integrityTasks.value[taskId] = result; - if (!result.active) { - integrityTasks.value[taskId].integrityCheckTaskId = null; - delete integrityTasks.value[taskId]; - } - } - - const stillActive = Object.keys(integrityTasks.value).length !== 0; - if (stillActive) { - integrityPollTimer = setTimeout(refreshIntegrityTasks, 10000); - } else { - integrityPollTimer = null; - } -} - async function onStartIntegrityCheck(backup) { - const [error, taskId] = await backupsModel.startIntegrityCheck(backup.id); + const [error] = await backupsModel.startIntegrityCheck(backup.id); if (error) return window.cloudron.onError(error); - backup.integrityCheckTaskId = taskId; - integrityTasks.value[taskId] = backup; + await refreshBackupList(); } async function onStopIntegrityCheck(backup) { const [error] = await backupsModel.stopIntegrityCheck(backup.id); if (error) return window.cloudron.onError(error); - delete integrityTasks.value[backup.integrityCheckTaskId]; - backup.integrityCheckTaskId = null; + await refreshBackupList(); } async function onRestoreSubmit() { @@ -300,15 +276,32 @@ function onClone(backup) { cloneDialog.value.open(backup, props.app.id); } +const INTEGRITY_POLL_INTERVAL_MS = 5000; +let integrityPollTimer = null; + +function scheduleIntegrityPoll() { + if (integrityPollTimer) return; + integrityPollTimer = setTimeout(async () => { + integrityPollTimer = null; + await refreshBackupList(); + if (backups.value.some(b => b.integrityCheckTask?.active)) { + scheduleIntegrityPoll(); + } + }, INTEGRITY_POLL_INTERVAL_MS); +} + async function refreshBackupList() { const [error, result] = await appsModel.backups(props.app.id); if (error) return console.error(error); for (const backup of result) { backup.site = backupSites.value.find(t => t.id === backup.siteId); - if (backup.integrityCheckTaskId) integrityTasks.value[backup.integrityCheckTaskId] = {}; } backups.value = result; + + if (result.some(b => b.integrityCheckTask?.active)) { + scheduleIntegrityPoll(); + } } onMounted(async () => { @@ -327,7 +320,6 @@ onMounted(async () => { await refreshBackupList(); await refreshTasks(); - await refreshIntegrityTasks(); busy.value = false; }); @@ -447,7 +439,7 @@ onUnmounted(() => { {{ prettyFileSize(item.stats.upload.size) }} - {{ item.stats.upload.fileCount }} file(s)