diff --git a/dashboard/public/translation/en.json b/dashboard/public/translation/en.json index 924925370..b918a1ad3 100644 --- a/dashboard/public/translation/en.json +++ b/dashboard/public/translation/en.json @@ -385,7 +385,9 @@ "date": "Created", "version": "Package version", "size": "Size", - "duration": "Backup duration" + "duration": "Backup duration", + "lastIntegrityCheck": "Last integrity check", + "integrityNever": "never" }, "configureBackupSchedule": { "title": "Configure Backup Schedule & Retention", @@ -503,7 +505,8 @@ "title": "Configure Backup Content" }, "useFileAndFileNameEncryption": "File and filename encryption used", - "useFileEncryption": "File encryption used" + "useFileEncryption": "File encryption used", + "checkIntegrity": "Check integrity" }, "branding": { "title": "Branding", diff --git a/dashboard/src/components/app/Backups.vue b/dashboard/src/components/app/Backups.vue index 20f4c591c..302067225 100644 --- a/dashboard/src/components/app/Backups.vue +++ b/dashboard/src/components/app/Backups.vue @@ -101,9 +101,9 @@ function createActionMenu(backup) { separator: true, }, { icon: 'fa-solid fa-key', - label: t('backups.checkIntegrity'), + label: backup.integrityCheckTaskId ? t('backups.stopIntegrity') : t('backups.checkIntegrity'), visible: props.app.accessLevel === 'admin', - action: onCheckIntegrity.bind(null, backup), + action: backup.integrityCheckTaskId ? onStopIntegrityCheck.bind(null, backup) : onStartIntegrityCheck.bind(null, backup), }]; } @@ -248,7 +248,10 @@ async function refreshIntegrityTasks() { const [error, result] = await tasksModel.get(taskId); if (error) continue; integrityTasks.value[taskId] = result; - if (!result.active) delete integrityTasks.value[taskId]; + if (!result.active) { + integrityTasks.value[taskId].integrityCheckTaskId = null; + delete integrityTasks.value[taskId]; + } } const stillActive = Object.keys(integrityTasks.value).length !== 0; @@ -259,12 +262,18 @@ async function refreshIntegrityTasks() { } } -async function onCheckIntegrity(backup) { - const [error, taskId] = await backupsModel.checkIntegrity(backup.id); +async function onStartIntegrityCheck(backup) { + const [error, taskId] = await backupsModel.startIntegrityCheck(backup.id); if (error) return window.cloudron.onError(error); backup.integrityCheckTaskId = taskId; - integrityTasks.value[taskId] = {}; - await refreshIntegrityTasks(); + integrityTasks.value[taskId] = backup; +} + +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; } async function onRestoreSubmit() { diff --git a/dashboard/src/models/BackupsModel.js b/dashboard/src/models/BackupsModel.js index 6f3c284a2..b68fc73a1 100644 --- a/dashboard/src/models/BackupsModel.js +++ b/dashboard/src/models/BackupsModel.js @@ -32,10 +32,10 @@ function create() { if (error || result.status !== 200) return [error || result]; return [null]; }, - async checkIntegrity(id) { + async startIntegrityCheck(id) { let error, result; try { - result = await fetcher.post(`${API_ORIGIN}/api/v1/backups/${id}/check_integrity`, {}, { access_token: accessToken }); + result = await fetcher.post(`${API_ORIGIN}/api/v1/backups/${id}/start_integrity_check`, {}, { access_token: accessToken }); } catch (e) { error = e; } @@ -43,6 +43,17 @@ function create() { if (error || result.status !== 201) return [error || result]; return [null, result.body.taskId]; }, + async stopIntegrityCheck(id) { + let error, result; + try { + result = await fetcher.post(`${API_ORIGIN}/api/v1/backups/${id}/stop_integrity_check`, {}, { access_token: accessToken }); + } catch (e) { + error = e; + } + + if (error || result.status !== 204) return [error || result]; + return [null]; + }, async get(id) { let error, result; try { diff --git a/src/backups.js b/src/backups.js index bbeab337c..728482462 100644 --- a/src/backups.js +++ b/src/backups.js @@ -13,6 +13,7 @@ exports = module.exports = { clearTasks, startIntegrityCheck, + stopIntegrityCheck, setIntegrityResult, BACKUP_IDENTIFIER_BOX: 'box', @@ -238,6 +239,15 @@ async function startIntegrityCheck(backup, auditSource) { return taskId; } +async function stopIntegrityCheck(backup, auditSource) { + assert.strictEqual(typeof backup, 'object'); + assert.strictEqual(typeof auditSource, 'object'); + + if (!backup.integrityCheckTaskId) throw new BoxError(BoxError.BAD_STATE, 'task is not active'); + + await tasks.stopTask(backup.integrityCheckTaskId); +} + async function clearTasks() { await database.query('UPDATE backups SET integrityCheckTaskId = NULL'); } diff --git a/src/routes/backups.js b/src/routes/backups.js index 0f049440f..e289049e0 100644 --- a/src/routes/backups.js +++ b/src/routes/backups.js @@ -7,7 +7,8 @@ exports = module.exports = { get, update, - checkIntegrity + startIntegrityCheck, + stopIntegrityCheck }; const assert = require('node:assert'), @@ -62,7 +63,7 @@ async function update(req, res, next) { next(new HttpSuccess(200, {})); } -async function checkIntegrity(req, res, next) { +async function startIntegrityCheck(req, res, next) { assert.strictEqual(typeof req.body, 'object'); const [error, taskId] = await safe(backups.startIntegrityCheck(req.resources.backup, AuditSource.fromRequest(req))); @@ -70,3 +71,12 @@ async function checkIntegrity(req, res, next) { next(new HttpSuccess(201, { taskId })); } + +async function stopIntegrityCheck(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + const [error] = await safe(backups.stopIntegrityCheck(req.resources.backup, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204, {})); +} diff --git a/src/server.js b/src/server.js index 0e9270c40..bfe0589e8 100644 --- a/src/server.js +++ b/src/server.js @@ -158,7 +158,8 @@ async function initializeExpressSync() { router.get ('/api/v1/backups', token, authorizeAdmin, routes.backups.list); router.get ('/api/v1/backups/:id', token, authorizeAdmin, routes.backups.load, routes.backups.get); router.post('/api/v1/backups/:id', json, token, authorizeAdmin, routes.backups.load, routes.backups.update); - router.post('/api/v1/backups/:id/check_integrity', json, token, authorizeAdmin, routes.backups.load, routes.backups.checkIntegrity); + router.post('/api/v1/backups/:id/start_integrity_check', json, token, authorizeAdmin, routes.backups.load, routes.backups.startIntegrityCheck); + router.post('/api/v1/backups/:id/stop_integrity_check', json, token, authorizeAdmin, routes.backups.load, routes.backups.stopIntegrityCheck); // backup site (destination) routes router.get ('/api/v1/backup_sites/', token, authorizeAdmin, routes.backupSites.list);