Files
cloudron-box/src/test/backups-test.js
2020-05-14 16:33:29 -07:00

347 lines
12 KiB
JavaScript

/* jslint node:true */
/* global it:false */
/* global describe:false */
/* global before:false */
/* global after:false */
'use strict';
var async = require('async'),
backupdb = require('../backupdb.js'),
backups = require('../backups.js'),
BoxError = require('../boxerror.js'),
createTree = require('./common.js').createTree,
database = require('../database'),
DataLayout = require('../datalayout.js'),
expect = require('expect.js'),
fs = require('fs'),
os = require('os'),
mkdirp = require('mkdirp'),
path = require('path'),
rimraf = require('rimraf'),
settings = require('../settings.js'),
tasks = require('../tasks.js');
function createBackup(callback) {
backups.startBackupTask({ username: 'test' }, function (error, taskId) { // this call does not wait for the backup!
if (error) return callback(error);
function waitForBackup() {
tasks.get(taskId, function (error, p) {
if (error) return callback(error);
if (p.percent !== 100) return setTimeout(waitForBackup, 1000);
if (p.errorMessage) return callback(new Error('backup failed:' + p));
if (!p.result) return callback(new Error('backup has no result:' + p));
backups.getByStatePaged(backupdb.BACKUP_STATE_NORMAL, 1, 1, function (error, result) {
if (error) return callback(error);
if (result.length !== 1) return callback(new Error('result is not of length 1'));
callback(null, result[0]);
});
});
}
setTimeout(waitForBackup, 1000);
});
}
function cleanupBackups(callback) {
backups.startCleanupTask({ username: 'test' }, function (error, taskId) { // this call does not wait for the backup!
if (error) return callback(error);
function waitForCleanup() {
tasks.get(taskId, function (error, p) {
if (error) return callback(error);
if (p.percent !== 100) return setTimeout(waitForCleanup, 1000);
if (p.errorMessage) return callback(new Error('backup failed:' + p.errorMessage));
callback();
});
}
setTimeout(waitForCleanup, 1000);
});
}
describe('backups', function () {
before(function (done) {
const BACKUP_DIR = path.join(os.tmpdir(), 'cloudron-backup-test');
async.series([
mkdirp.bind(null, BACKUP_DIR),
database.initialize,
database._clear,
settings.setBackupConfig.bind(null, {
provider: 'filesystem',
password: 'supersecret',
backupFolder: BACKUP_DIR,
retentionPolicy: { keepWithin: 1 },
format: 'tgz'
})
], done);
});
after(function (done) {
async.series([
database._clear
], done);
});
describe('cleanup', function () {
var BACKUP_0 = {
id: 'backup-box-0',
encryptionVersion: null,
packageVersion: '1.0.0',
type: backupdb.BACKUP_TYPE_BOX,
dependsOn: [ 'backup-app-00', 'backup-app-01' ],
manifest: null,
format: 'tgz'
};
var BACKUP_0_APP_0 = {
id: 'backup-app-00',
encryptionVersion: null,
packageVersion: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
format: 'tgz'
};
var BACKUP_0_APP_1 = {
id: 'backup-app-01',
encryptionVersion: null,
packageVersion: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
format: 'tgz'
};
var BACKUP_1 = {
id: 'backup-box-1',
encryptionVersion: null,
packageVersion: '1.0.0',
type: backupdb.BACKUP_TYPE_BOX,
dependsOn: [ 'backup-app-10', 'backup-app-11' ],
manifest: null,
format: 'tgz'
};
var BACKUP_1_APP_0 = {
id: 'backup-app-10',
encryptionVersion: null,
packageVersion: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
format: 'tgz'
};
var BACKUP_1_APP_1 = {
id: 'backup-app-11',
encryptionVersion: null,
packageVersion: '1.0.0',
type: backupdb.BACKUP_TYPE_APP,
dependsOn: [],
manifest: null,
format: 'tgz'
};
it('succeeds without backups', function (done) {
cleanupBackups(done);
});
it('succeeds with box backups, keeps latest', function (done) {
async.eachSeries([[ BACKUP_0, BACKUP_0_APP_0, BACKUP_0_APP_1 ], [ BACKUP_1, BACKUP_1_APP_0, BACKUP_1_APP_1 ]], function (backup, callback) {
// space out backups
setTimeout(function () {
async.each(backup, (b, done) => backupdb.add(b.id, b, done), callback);
}, 2000);
}, function (error) {
expect(error).to.not.be.ok();
cleanupBackups(function (error) {
expect(error).to.not.be.ok();
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(BACKUP_1.id);
// check that app backups are gone as well
backupdb.get(BACKUP_0_APP_0.id, function (error) {
expect(error).to.be.a(BoxError);
expect(error.reason).to.equal(BoxError.NOT_FOUND);
done();
});
});
});
});
});
it('does not remove expired backups if only one left', function (done) {
cleanupBackups(function (error) {
expect(error).to.not.be.ok();
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_BOX, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(1);
expect(result[0].id).to.equal(BACKUP_1.id);
// check that app backups are also still there
backupdb.get(BACKUP_1_APP_0.id, function (error, result) {
expect(error).to.not.be.ok();
expect(result.id).to.equal(BACKUP_1_APP_0.id);
done();
});
});
});
});
it('succeeds for app backups not referenced by a box backup', function (done) {
async.eachSeries([BACKUP_0_APP_0, BACKUP_0_APP_1], (b, done) => backupdb.add(b.id, b, done), function (error) {
expect(error).to.not.be.ok();
// wait for expiration
setTimeout(function () {
cleanupBackups(function (error) {
expect(error).to.not.be.ok();
backupdb.getByTypePaged(backupdb.BACKUP_TYPE_APP, 1, 1000, function (error, result) {
expect(error).to.not.be.ok();
expect(result.length).to.equal(2);
done();
});
});
}, 2000);
});
});
});
describe('fs meta data', function () {
var tmpdir;
before(function () {
tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'backups-test'));
});
after(function () {
rimraf.sync(tmpdir);
});
it('saves special files', function (done) {
createTree(tmpdir, { 'data': { 'subdir': { 'emptydir': { } } }, 'dir2': { 'file': 'stuff' } });
fs.chmodSync(path.join(tmpdir, 'dir2/file'), parseInt('0755', 8));
let dataLayout = new DataLayout(tmpdir, []);
backups._saveFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`, function (error) {
expect(error).to.not.be.ok();
var emptyDirs = JSON.parse(fs.readFileSync(path.join(tmpdir, 'fsmetadata.json'), 'utf8')).emptyDirs;
expect(emptyDirs).to.eql(['./data/subdir/emptydir']);
var execFiles = JSON.parse(fs.readFileSync(path.join(tmpdir, 'fsmetadata.json'), 'utf8')).execFiles;
expect(execFiles).to.eql(['./dir2/file']);
done();
});
});
it('restores special files', function (done) {
rimraf.sync(path.join(tmpdir, 'data'));
expect(fs.existsSync(path.join(tmpdir, 'data/subdir/emptydir'))).to.be(false); // just make sure rimraf worked
let dataLayout = new DataLayout(tmpdir, []);
backups._restoreFsMetadata(dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`, function (error) {
expect(error).to.not.be.ok();
expect(fs.existsSync(path.join(tmpdir, 'data/subdir/emptydir'))).to.be(true);
var mode = fs.statSync(path.join(tmpdir, 'dir2/file')).mode;
expect(mode & ~fs.constants.S_IFREG).to.be(parseInt('0755', 8));
done();
});
});
});
describe('filesystem', function () {
var backupInfo1;
var gBackupConfig = {
provider: 'filesystem',
backupFolder: path.join(os.tmpdir(), 'backups-test-filesystem'),
format: 'tgz'
};
before(function (done) {
rimraf.sync(gBackupConfig.backupFolder);
done();
});
after(function (done) {
rimraf.sync(gBackupConfig.backupFolder);
done();
});
it('fails to set backup config for non-existing folder', function (done) {
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be.a(BoxError);
expect(error.reason).to.equal(BoxError.BAD_FIELD);
done();
});
});
it('succeeds to set backup config', function (done) {
mkdirp.sync(gBackupConfig.backupFolder);
settings.setBackupConfig(gBackupConfig, function (error) {
expect(error).to.be(null);
done();
});
});
it('can backup', function (done) {
// arch only has maria db which lacks some mysqldump options we need, this is only here to allow running the tests :-/
if (require('child_process').execSync('/usr/bin/mysqldump --version').toString().indexOf('MariaDB') !== -1) return done();
createBackup(function (error, result) {
console.dir(error);
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2);
backupInfo1 = result;
done();
});
});
it('can take another backup', function (done) {
// arch only has maria db which lacks some mysqldump options we need, this is only here to allow running the tests :-/
if (require('child_process').execSync('/usr/bin/mysqldump --version').toString().indexOf('MariaDB') !== -1) return done();
createBackup(function (error, result) {
expect(error).to.be(null);
expect(fs.statSync(path.join(gBackupConfig.backupFolder, 'snapshot/box.tar.gz')).nlink).to.be(2); // hard linked to a rotated backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${result.id}.tar.gz`)).nlink).to.be(2); // hard linked to new backup
expect(fs.statSync(path.join(gBackupConfig.backupFolder, `${backupInfo1.id}.tar.gz`)).nlink).to.be(1); // not hard linked anymore
done();
});
});
});
});