9f2eefcbb3
The UI is polling for the taskId, might as well attach it
272 lines
11 KiB
JavaScript
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,
|
|
};
|