add integrity check to system backups

This commit is contained in:
Girish Ramakrishnan
2026-02-15 14:59:27 +01:00
parent c7b321315c
commit adf884c2c4
3 changed files with 80 additions and 34 deletions
@@ -137,7 +137,7 @@ defineExpose({
<div class="info-row">
<div class="info-label">{{ $t('backups.backupDetails.lastIntegrityCheck') }}</div>
<div class="info-value">
<span v-if="backup.integrityCheckTaskId">{{ $t('backups.backupDetails.integrityInProgress') }}</span>
<span v-if="backup.integrityCheckTask?.active">{{ $t('backups.backupDetails.integrityInProgress') }}</span>
<span v-else-if="backup.lastIntegrityCheckTime">{{ prettyLongDate(backup.lastIntegrityCheckTime) }}</span>
<span v-else>{{ $t('backups.backupDetails.integrityNever') }}</span>
</div>
+55 -1
View File
@@ -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 });
</script>
@@ -291,6 +336,15 @@ defineExpose({ refresh });
<template #site="{ item:backup }">{{ backup.site.name }}</template>
<template #integrity="{ item:backup }">
<Spinner v-if="backup.integrityCheckTask?.active" style="min-width: 0;"/>
<div v-else-if="backup.lastIntegrityCheckTime" style="display: flex; align-items: center;">
<i v-if="backup.integrityCheckStatus === 'passed'" class="fa-solid fa-check-circle" v-tooltip="prettyLongDate(backup.lastIntegrityCheckTime)"></i>
<i v-else class="fa-solid fa-times-circle" v-tooltip="prettyLongDate(backup.lastIntegrityCheckTime)"></i>
</div>
<div v-else>-</div>
</template>
<template #actions="{ item:backup }">
<ActionBar :actions="createActionMenu(backup)"/>
</template>
+24 -32
View File
@@ -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(() => {
<span v-if="item.stats?.upload">{{ prettyFileSize(item.stats.upload.size) }} - {{ item.stats.upload.fileCount }} file(s)</span>
</template>
<template #integrity="{ item }">
<Spinner v-if="item.integrityCheckTaskId && integrityTasks[item.integrityCheckTaskId]" style="min-width: 0;"/>
<Spinner v-if="item.integrityCheckTask?.active" style="min-width: 0;"/>
<div v-else-if="item.lastIntegrityCheckTime" style="display: flex; align-items: center;">
<i v-if="item.integrityCheckStatus === 'passed'" class="fa-solid fa-check-circle" v-tooltip="prettyLongDate(item.lastIntegrityCheckTime)"></i>
<i v-else class="fa-solid fa-times-circle" v-tooltip="prettyLongDate(item.lastIntegrityCheckTime)"></i>