backups: add backup multiple targets
This commit is contained in:
+122
-45
@@ -1,17 +1,23 @@
|
||||
'use strict';
|
||||
|
||||
exports = module.exports = {
|
||||
load,
|
||||
|
||||
list,
|
||||
get,
|
||||
add,
|
||||
del,
|
||||
|
||||
// separate update routes to skip (slow) storage validation
|
||||
setConfig,
|
||||
setLimits,
|
||||
setSchedule,
|
||||
setRetention,
|
||||
|
||||
create,
|
||||
cleanup,
|
||||
remount,
|
||||
getMountStatus,
|
||||
|
||||
getConfig,
|
||||
setStorage,
|
||||
setLimits,
|
||||
|
||||
getPolicy,
|
||||
setPolicy
|
||||
};
|
||||
|
||||
const assert = require('assert'),
|
||||
@@ -22,40 +28,122 @@ const assert = require('assert'),
|
||||
HttpSuccess = require('@cloudron/connect-lastmile').HttpSuccess,
|
||||
safe = require('safetydance');
|
||||
|
||||
async function load(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
const [error, result] = await safe(backupTargets.get(req.params.id));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
if (!result) return next(new HttpError(404, 'Backup target not found'));
|
||||
|
||||
req.resources.backupTarget = result;
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
async function get(req, res, next) {
|
||||
assert.strictEqual(typeof req.params.id, 'string');
|
||||
|
||||
next(new HttpSuccess(200, backupTargets.removePrivateFields(req.resources.backupTarget)));
|
||||
}
|
||||
|
||||
async function list(req, res, next) {
|
||||
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, result] = await safe(backupTargets.list(page, perPage));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, { backupTargets: result.map(backupTargets.removePrivateFields) }));
|
||||
}
|
||||
|
||||
// Target 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 add(req, res, next) {
|
||||
assert.strictEqual(typeof req.body, 'object');
|
||||
|
||||
const { label, format, config } = req.body;
|
||||
|
||||
if (typeof format !== 'string') return next(new HttpError(400, 'format must be a string'));
|
||||
if (typeof label !== 'string') return next(new HttpError(400, 'label must be a string'));
|
||||
if (typeof provider !== 'string') return next(new HttpError(400, 'provider is required'));
|
||||
|
||||
// provider specific options are validated by provider backends
|
||||
if (!config || typeof req.body.config !== 'object') return next(new HttpError(400, 'config is required'));
|
||||
|
||||
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'));
|
||||
|
||||
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 ('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, id] = await safe(backupTargets.add(req.body));
|
||||
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.backupTarget, 'object');
|
||||
|
||||
const [error] = await safe(backupTargets.del(req.resources.backupTarget, AuditSource.fromRequest(req)));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(204));
|
||||
}
|
||||
|
||||
async function create(req, res, next) {
|
||||
const [error, taskId] = await safe(backupTargets.startBackupTask(AuditSource.fromRequest(req)));
|
||||
assert.strictEqual(typeof req.resources.backupTarget, 'object');
|
||||
|
||||
const [error, taskId] = await safe(backupTargets.startBackupTask(req.resources.backupTarget, 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)));
|
||||
assert.strictEqual(typeof req.resources.backupTarget, 'object');
|
||||
|
||||
const [error, taskId] = await safe(backupTargets.startCleanupTask(req.resources.backupTarget, 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());
|
||||
assert.strictEqual(typeof req.resources.backupTarget, 'object');
|
||||
|
||||
const [error] = await safe(backupTargets.remount(req.resources.backupTarget));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(202, {}));
|
||||
}
|
||||
|
||||
async function getMountStatus(req, res, next) {
|
||||
const [error, mountStatus] = await safe(backupTargets.getMountStatus());
|
||||
assert.strictEqual(typeof req.resources.backupTarget, 'object');
|
||||
|
||||
|
||||
const [error, mountStatus] = await safe(backupTargets.getMountStatus(req.resources.backupTarget));
|
||||
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');
|
||||
|
||||
@@ -84,55 +172,44 @@ async function setLimits(req, res, next) {
|
||||
|
||||
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));
|
||||
const [error] = await safe(backupTargets.setLimits(req.resources.backupTarget, 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) {
|
||||
async function setConfig(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));
|
||||
const [error] = await safe(backupTargets.setConfig(req.resources.backupTarget, 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) {
|
||||
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'));
|
||||
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));
|
||||
const [error] = await safe(backupTargets.setSchedule(req.resources.backupTarget, req.body.schedule));
|
||||
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(backupTargets.setRetention(req.resources.backupTarget, req.body.retention));
|
||||
if (error) return next(BoxError.toHttpError(error));
|
||||
|
||||
next(new HttpSuccess(200, {}));
|
||||
|
||||
@@ -119,11 +119,12 @@ describe('Backups API', function () {
|
||||
};
|
||||
|
||||
it('can get backup_config (default)', async function () {
|
||||
const response = await superagent.get(`${serverUrl}/api/v1/backups/config`)
|
||||
const response = await superagent.get(`${serverUrl}/api/v1/backup_targets`)
|
||||
.query({ access_token: owner.token });
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body).to.eql(defaultConfig);
|
||||
expect(response.body.config).to.eql(defaultConfig);
|
||||
expect(response.body.config).to.eql(defaultConfig);
|
||||
});
|
||||
|
||||
it('cannot set backup_config without provider', async function () {
|
||||
|
||||
@@ -115,7 +115,7 @@ async function setupServer() {
|
||||
await database.initialize();
|
||||
await database._clear();
|
||||
await appstore._setApiServerOrigin(exports.mockApiServerOrigin);
|
||||
await backupTargets._addDefaultTarget();
|
||||
await backupTargets._addDefault();
|
||||
await oidcServer.stop();
|
||||
await server.start();
|
||||
debug('Set up server complete');
|
||||
|
||||
Reference in New Issue
Block a user