Files
cloudron-box/src/routes/backuptargets.js
T
2025-07-24 18:54:10 +02:00

140 lines
5.9 KiB
JavaScript

'use strict';
exports = module.exports = {
create,
cleanup,
remount,
getMountStatus,
getConfig,
setStorage,
setLimits,
getPolicy,
setPolicy
};
const assert = require('assert'),
AuditSource = require('../auditsource.js'),
backupTargets = require('../backuptargets.js'),
BoxError = require('../boxerror.js'),
HttpError = require('@cloudron/connect-lastmile').HttpError,
HttpSuccess = require('@cloudron/connect-lastmile').HttpSuccess,
safe = require('safetydance');
async function create(req, res, next) {
const [error, taskId] = await safe(backupTargets.startBackupTask(AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
}
async function cleanup(req, res, next) {
const [error, taskId] = await safe(backupTargets.startCleanupTask(AuditSource.fromRequest(req)));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, { taskId }));
}
async function remount(req, res, next) {
const [error] = await safe(backupTargets.remount());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(202, {}));
}
async function getMountStatus(req, res, next) {
const [error, mountStatus] = await safe(backupTargets.getMountStatus());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, mountStatus));
}
async function getConfig(req, res, next) {
const [error, backupConfig] = await safe(backupTargets.getConfig());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, backupTargets.removePrivateFields(backupConfig)));
}
async function setLimits(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
const limits = req.body;
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(backupTargets.setLimits(req.body));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
// storage has three parts. these fields are merged into one top level object
// 1. format. rsync or tgz
// 2. config. the 'provider' (see api() function in src/storage.js) differentiates further options
// s3 providers - accessKeyId, secretAccessKey, bucket, prefix etc . see s3.js
// gcs - bucket, prefix, projectId, credentials . see gcs.js
// ext4/xfs/disk (managed providers) - mountOptions (diskPath), prefix, noHardlinks. disk is legacy.
// nfs/cifs/sshfs (managed providers) - mountOptions (host/username/password/seal/privateKey etc), prefix, noHardlinks
// filesystem - backupFolder, noHardlinks
// mountpoint - mountPoint, prefix, noHardlinks
// 3. encryption. password and encryptedFilenames
async function setStorage(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
// provider specific options are validated by provider backends
if (typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required'));
if (typeof req.body.format !== 'string') return next(new HttpError(400, 'format must be a string'));
if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string'));
if ('encryptedFilenames' in req.body && typeof req.body.encryptedFilenames !== 'boolean') return next(new HttpError(400, 'encryptedFilenames must be a boolean'));
// testing the backup using put/del takes a bit of time at times
req.clearTimeout();
const [error] = await safe(backupTargets.setStorage(req.body));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}
async function getPolicy(req, res, next) {
const [error, policy] = await safe(backupTargets.getPolicy());
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, { policy }));
}
async function setPolicy(req, res, next) {
assert.strictEqual(typeof req.body, 'object');
if (typeof req.body.schedule !== 'string') return next(new HttpError(400, 'schedule is required'));
if (!req.body.retention || typeof req.body.retention !== 'object') return next(new HttpError(400, 'retention is required'));
const [error] = await safe(backupTargets.setPolicy(req.body));
if (error) return next(BoxError.toHttpError(error));
next(new HttpSuccess(200, {}));
}