Files
cloudron-box/src/routes/backupsites.js
T
Girish Ramakrishnan 9f2eefcbb3 embed integrity check task in backup API responses
The UI is polling for the taskId, might as well attach it
2026-02-15 14:11:56 +01:00

272 lines
11 KiB
JavaScript

import assert from 'node:assert';
import AuditSource from '../auditsource.js';
import backups from '../backups.js';
import backupSites from '../backupsites.js';
import BoxError from '../boxerror.js';
import { HttpError } from '@cloudron/connect-lastmile';
import { HttpSuccess } from '@cloudron/connect-lastmile';
import safe from 'safetydance';
async function load(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
const [error, result] = await safe(backupSites.get(req.params.id));
if (error) return next(BoxError.toHttpError(error));
if (!result) return next(new HttpError(404, 'Backup site not found'));
req.resources.backupSite = result;
next();
}
async function get(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
next(new HttpSuccess(200, backupSites.removePrivateFields(req.resources.backupSite)));
}
async function list(req, res, next) {
const [error, result] = await safe(backupSites.list());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backupSites: result.map(backupSites.removePrivateFields) }));
}
async function add(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const { name, format, provider, contents, config, schedule, retention } = req.body;
if (typeof format !== 'string') return next(new HttpError(400, 'format must be a string'));
if (typeof name !== 'string') return next(new HttpError(400, 'name must be a string'));
if (typeof provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof contents !== 'object') return next(new HttpError(400, 'contents is required'));
// provider specific options are validated by provider backends
if (!config || typeof config !== 'object') return next(new HttpError(400, 'config is required'));
if (typeof schedule !== 'string') return next(new HttpError(400, 'schedule is required'));
if (!retention || typeof retention !== 'object') return next(new HttpError(400, 'retention is required'));
if ('limits' in req.body && typeof req.body.limits !== 'object') return next(new HttpError(400, 'limits must be an object'));
if ('encryptionPassword' in req.body && typeof req.body.encryptionPassword !== 'string') return next(new HttpError(400, 'encryptionPassword must be a string'));
if ('encryptionPasswordHint' in req.body && typeof req.body.encryptionPasswordHint !== 'string') return next(new HttpError(400, 'encryptionPasswordHint must be a string'));
if ('encryptedFilenames' in req.body && typeof req.body.encryptedFilenames !== 'boolean') return next(new HttpError(400, 'encryptedFilenames must be a boolean'));
if ('enableForUpdates' in req.body && typeof req.body.enableForUpdates !== 'boolean') return next(new HttpError(400, 'enableForUpdates must be a boolean'));
// testing the backup using put/del takes a bit of time at times
req.clearTimeout();
const [error, id] = await safe(backupSites.add(req.body, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { id }));
}
async function del(req, res, next) {
assert.strictEqual(typeof req.params.id, 'string');
assert.strictEqual(typeof req.resources.backupSite, 'object');
const [error] = await safe(backupSites.del(req.resources.backupSite, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(204));
}
async function setLimits(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const { limits } = req.body;
if (!limits || typeof limits !== 'object') return next(new HttpError(400, 'limits is required'));
if ('syncConcurrency' in limits) {
if (typeof limits.syncConcurrency !== 'number') return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
if (limits.syncConcurrency < 1) return next(new HttpError(400, 'syncConcurrency must be a positive integer'));
}
if ('copyConcurrency' in limits) {
if (typeof limits.copyConcurrency !== 'number') return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
if (limits.copyConcurrency < 1) return next(new HttpError(400, 'copyConcurrency must be a positive integer'));
}
if ('downloadConcurrency' in limits) {
if (typeof limits.downloadConcurrency !== 'number') return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
if (limits.downloadConcurrency < 1) return next(new HttpError(400, 'downloadConcurrency must be a positive integer'));
}
if ('deleteConcurrency' in limits) {
if (typeof limits.deleteConcurrency !== 'number') return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
if (limits.deleteConcurrency < 1) return next(new HttpError(400, 'deleteConcurrency must be a positive integer'));
}
if ('uploadPartSize' in limits) {
if (typeof limits.uploadPartSize !== 'number') return next(new HttpError(400, 'uploadPartSize must be a positive integer'));
if (limits.uploadPartSize < 1) return next(new HttpError(400, 'uploadPartSize must be a positive integer'));
}
if ('memoryLimit' in limits && typeof limits.memoryLimit !== 'number') return next(new HttpError(400, 'memoryLimit must be a positive integer'));
const [error] = await safe(backupSites.setLimits(req.resources.backupSite, limits, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setConfig(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config is required'));
// testing the backup using put/del takes a bit of time at times
req.clearTimeout();
const [error] = await safe(backupSites.setConfig(req.resources.backupSite, req.body.config, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setSchedule(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.schedule !== 'string') return next(new HttpError(400, 'schedule is required'));
const [error] = await safe(backupSites.setSchedule(req.resources.backupSite, req.body.schedule, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setRetention(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (!req.body.retention || typeof req.body.retention !== 'object') return next(new HttpError(400, 'retention is required'));
const [error] = await safe(backupSites.setRetention(req.resources.backupSite, req.body.retention, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setEnabledForUpdates(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.enable !== 'boolean') return next(new HttpError(400, 'enable is required'));
const [error] = await safe(backupSites.setEnabledForUpdates(req.resources.backupSite, req.body.enable, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setContents(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.contents !== 'object') return next(new HttpError(400, 'contents must be an object'));
const [error] = await safe(backupSites.setContents(req.resources.backupSite, req.body.contents, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setEncryption(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if ('encryptionPassword' in req.body) {
if (req.body.encryptionPassword === null || typeof req.body.encryptionPassword !== 'string') return next(new HttpError(400, 'encryptionPassword must be a string or null'));
}
if ('encryptionPasswordHint' in req.body && typeof req.body.encryptionPasswordHint !== 'string') return next(new HttpError(400, 'encryptionPasswordHint must be a string'));
if ('encryptedFilenames' in req.body && typeof req.body.encryptedFilenames !== 'boolean') return next(new HttpError(400, 'encryptedFilenames must be a boolean'));
const [error] = await safe(backupSites.setEncryption(req.resources.backupSite, req.body, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function setName(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.name !== 'string') return next(new HttpError(400, 'name is required'));
const [error] = await safe(backupSites.setName(req.resources.backupSite, req.body.name, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function createBackup(req, res, next) {
assert.strictEqual(typeof req.resources.backupSite, 'object');
const [error, taskId] = await safe(backupSites.startBackupTask(req.resources.backupSite, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
}
async function cleanup(req, res, next) {
assert.strictEqual(typeof req.resources.backupSite, 'object');
const [error, taskId] = await safe(backupSites.startCleanupTask(req.resources.backupSite, AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
}
async function remount(req, res, next) {
assert.strictEqual(typeof req.resources.backupSite, 'object');
const [error] = await safe(backupSites.remount(req.resources.backupSite));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
}
async function getStatus(req, res, next) {
assert.strictEqual(typeof req.resources.backupSite, 'object');
const [error, mountStatus] = await safe(backupSites.getStatus(req.resources.backupSite));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, mountStatus));
}
async function listBackups(req, res, next) {
assert.strictEqual(typeof req.resources.backupSite, 'object');
const page = typeof req.query.page === 'string' ? parseInt(req.query.page) : 1;
if (!page || page < 0) return next(new HttpError(400, 'page query param has to be a postive number'));
const perPage = typeof req.query.per_page === 'string'? parseInt(req.query.per_page) : 25;
if (!perPage || perPage < 0) return next(new HttpError(400, 'per_page query param has to be a postive number'));
const [error, results] = await safe(backups.listByTypePaged(backups.BACKUP_TYPE_BOX, req.resources.backupSite.id, page, perPage));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { backups: results.map(backups.removePrivateFields) }));
}
export default {
load,
list,
get,
add,
del,
// separate update routes to skip (slow) storage validation
setConfig,
setLimits,
setSchedule,
setRetention,
setEnabledForUpdates,
setName,
setContents,
setEncryption,
listBackups,
createBackup,
cleanup,
remount,
getStatus,
};