520 lines
21 KiB
JavaScript
520 lines
21 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
fullBackup,
|
|
|
|
restore,
|
|
|
|
backupApp,
|
|
downloadApp,
|
|
|
|
backupMail,
|
|
downloadMail,
|
|
|
|
upload,
|
|
};
|
|
|
|
const apps = require('./apps.js'),
|
|
assert = require('assert'),
|
|
backupFormat = require('./backupformat.js'),
|
|
backups = require('./backups.js'),
|
|
BoxError = require('./boxerror.js'),
|
|
constants = require('./constants.js'),
|
|
DataLayout = require('./datalayout.js'),
|
|
database = require('./database.js'),
|
|
debug = require('debug')('box:backuptask'),
|
|
df = require('./df.js'),
|
|
path = require('path'),
|
|
paths = require('./paths.js'),
|
|
safe = require('safetydance'),
|
|
services = require('./services.js'),
|
|
shell = require('./shell.js'),
|
|
storage = require('./storage.js');
|
|
|
|
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
|
|
|
|
function canBackupApp(app) {
|
|
// only backup apps that are installed or specific pending states
|
|
|
|
// stopped apps cannot be backed up because addons might be down (redis)
|
|
if (app.runState === apps.RSTATE_STOPPED) return false;
|
|
|
|
// we used to check the health here but that doesn't work for stopped apps. it's better to just fail
|
|
// and inform the user if the backup fails and the app addons have not been setup yet.
|
|
return app.installationState === apps.ISTATE_INSTALLED ||
|
|
app.installationState === apps.ISTATE_PENDING_CONFIGURE ||
|
|
app.installationState === apps.ISTATE_PENDING_BACKUP || // called from apptask
|
|
app.installationState === apps.ISTATE_PENDING_UPDATE; // called from apptask
|
|
}
|
|
|
|
async function checkPreconditions(backupConfig, dataLayout) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
|
|
// check mount status before uploading
|
|
const status = await backups.ensureMounted();
|
|
debug(`upload: mount point status is ${JSON.stringify(status)}`);
|
|
if (status.state !== 'active') throw new BoxError(BoxError.MOUNT_ERROR, `Backup endpoint is not active: ${status.message}`);
|
|
|
|
// check availabe size. this requires root for df to work
|
|
const available = await storage.api(backupConfig.provider).getAvailableSize(backupConfig);
|
|
let used = 0;
|
|
for (const localPath of dataLayout.localPaths()) {
|
|
debug(`checkPreconditions: getting disk usage of ${localPath}`);
|
|
const result = await shell.execArgs('checkPreconditions', 'du', [ '-Dsb', '--exclude=*.lock', '--exclude=dovecot.list.index.log.*', localPath], {});
|
|
used += parseInt(result, 10);
|
|
}
|
|
|
|
debug(`checkPreconditions: total required=${used} available=${available}`);
|
|
|
|
const needed = 0.6 * used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards. aim for 60% because rsync/tgz won't need full 100%
|
|
if (available <= needed) throw new BoxError(BoxError.FS_ERROR, `Not enough disk space for backup. Needed: ${df.prettyBytes(needed)} Available: ${df.prettyBytes(available)}`);
|
|
}
|
|
|
|
// this function is called via backupupload (since it needs root to traverse app's directory)
|
|
async function upload(remotePath, format, dataLayoutString, progressCallback) {
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
assert.strictEqual(typeof format, 'string');
|
|
assert.strictEqual(typeof dataLayoutString, 'string');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
debug(`upload: path ${remotePath} format ${format} dataLayout ${dataLayoutString}`);
|
|
|
|
const dataLayout = DataLayout.fromString(dataLayoutString);
|
|
const backupConfig = await backups.getConfig();
|
|
|
|
await checkPreconditions(backupConfig, dataLayout);
|
|
|
|
await backupFormat.api(format).upload(backupConfig, remotePath, dataLayout, progressCallback);
|
|
}
|
|
|
|
async function download(backupConfig, remotePath, format, dataLayout, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
assert.strictEqual(typeof format, 'string');
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
debug(`download: Downloading ${remotePath} of format ${format} to ${dataLayout.toString()}`);
|
|
|
|
await backupFormat.api(format).download(backupConfig, remotePath, dataLayout, progressCallback);
|
|
}
|
|
|
|
async function restore(backupConfig, remotePath, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
|
|
if (!boxDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`);
|
|
const dataLayout = new DataLayout(boxDataDir, []);
|
|
|
|
await download(backupConfig, remotePath, backupConfig.format, dataLayout, progressCallback);
|
|
|
|
debug('restore: download completed, importing database');
|
|
|
|
await database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`);
|
|
|
|
debug('restore: database imported');
|
|
}
|
|
|
|
async function downloadApp(app, restoreConfig, progressCallback) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof restoreConfig, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
|
if (!appDataDir) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
|
|
const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []);
|
|
|
|
const startTime = new Date();
|
|
const backupConfig = restoreConfig.backupConfig || await backups.getConfig();
|
|
|
|
await download(backupConfig, restoreConfig.remotePath, restoreConfig.backupFormat, dataLayout, progressCallback);
|
|
debug('downloadApp: time: %s', (new Date() - startTime)/1000);
|
|
}
|
|
|
|
async function runBackupUpload(uploadConfig, progressCallback) {
|
|
assert.strictEqual(typeof uploadConfig, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const { remotePath, backupConfig, dataLayout, progressTag } = uploadConfig;
|
|
assert.strictEqual(typeof remotePath, 'string');
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof progressTag, 'string');
|
|
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
|
|
|
|
// https://stackoverflow.com/questions/48387040/node-js-recommended-max-old-space-size
|
|
const envCopy = Object.assign({}, process.env);
|
|
if (backupConfig.limits?.memoryLimit >= 2*1024*1024*1024) {
|
|
const heapSize = Math.min((backupConfig.limits.memoryLimit/1024/1024) - 256, 8192);
|
|
debug(`runBackupUpload: adjusting heap size to ${heapSize}M`);
|
|
envCopy.NODE_OPTIONS = `--max-old-space-size=${heapSize}`;
|
|
}
|
|
|
|
let result = ''; // the script communicates error result as a string
|
|
function onMessage(progress) { // this is { message } or { result }
|
|
if ('message' in progress) return progressCallback({ message: `${progress.message} (${progressTag})` });
|
|
debug(`runBackupUpload: result - ${JSON.stringify(progress)}`);
|
|
result = progress.result;
|
|
}
|
|
|
|
const [error] = await safe(shell.promises.sudo(`backup-${remotePath}`, [ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true, onMessage }));
|
|
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
|
|
throw new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed');
|
|
} else if (error && error.code === 50) { // exited with error
|
|
throw new BoxError(BoxError.EXTERNAL_ERROR, result);
|
|
}
|
|
}
|
|
|
|
async function snapshotBox(progressCallback) {
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
progressCallback({ message: 'Snapshotting box' });
|
|
|
|
const startTime = new Date();
|
|
|
|
await database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`);
|
|
debug(`snapshotBox: took ${(new Date() - startTime)/1000} seconds`);
|
|
}
|
|
|
|
async function uploadBoxSnapshot(backupConfig, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
await snapshotBox(progressCallback);
|
|
|
|
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
|
|
if (!boxDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`);
|
|
|
|
const uploadConfig = {
|
|
remotePath: 'snapshot/box',
|
|
backupConfig,
|
|
dataLayout: new DataLayout(boxDataDir, []),
|
|
progressTag: 'box'
|
|
};
|
|
|
|
progressCallback({ message: 'Uploading box snapshot' });
|
|
|
|
const startTime = new Date();
|
|
|
|
await runBackupUpload(uploadConfig, progressCallback);
|
|
|
|
debug(`uploadBoxSnapshot: took ${(new Date() - startTime)/1000} seconds`);
|
|
|
|
await backups.setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: backupConfig.format });
|
|
}
|
|
|
|
async function copy(backupConfig, srcRemotePath, destRemotePath, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof srcRemotePath, 'string');
|
|
assert.strictEqual(typeof destRemotePath, 'string');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const { provider, format } = backupConfig;
|
|
const oldFilePath = backupFormat.api(format).getBackupFilePath(backupConfig, srcRemotePath);
|
|
const newFilePath = backupFormat.api(format).getBackupFilePath(backupConfig, destRemotePath);
|
|
|
|
const startTime = new Date();
|
|
const [copyError] = await safe(storage.api(provider).copy(backupConfig, oldFilePath, newFilePath, progressCallback));
|
|
if (copyError) {
|
|
debug(`copy: copied to ${destRemotePath} errored. error: ${copyError.message}`);
|
|
throw copyError;
|
|
}
|
|
debug(`copy: copied successfully to ${destRemotePath}. Took ${(new Date() - startTime)/1000} seconds`);
|
|
}
|
|
|
|
async function rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof tag, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert(Array.isArray(dependsOn));
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const remotePath = `${tag}/box_v${constants.VERSION}`;
|
|
const format = backupConfig.format;
|
|
|
|
debug(`rotateBoxBackup: rotating to id ${remotePath}`);
|
|
|
|
const data = {
|
|
remotePath,
|
|
encryptionVersion: backupConfig.encryption ? 2 : null,
|
|
packageVersion: constants.VERSION,
|
|
type: backups.BACKUP_TYPE_BOX,
|
|
state: backups.BACKUP_STATE_CREATING,
|
|
identifier: backups.BACKUP_IDENTIFIER_BOX,
|
|
dependsOn,
|
|
manifest: null,
|
|
format,
|
|
preserveSecs: options.preserveSecs || 0
|
|
};
|
|
|
|
const id = await backups.add(data);
|
|
const [error] = await safe(copy(backupConfig, 'snapshot/box', remotePath, progressCallback));
|
|
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
|
|
await backups.setState(id, state);
|
|
if (error) throw error;
|
|
|
|
return id;
|
|
}
|
|
|
|
async function backupBox(dependsOn, tag, options, progressCallback) {
|
|
assert(Array.isArray(dependsOn));
|
|
assert.strictEqual(typeof tag, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const backupConfig = await backups.getConfig();
|
|
|
|
await uploadBoxSnapshot(backupConfig, progressCallback);
|
|
return await rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCallback);
|
|
}
|
|
|
|
async function rotateAppBackup(backupConfig, app, tag, options, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof tag, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const snapshotInfo = backups.getSnapshotInfo(app.id);
|
|
|
|
const manifest = snapshotInfo.restoreConfig ? snapshotInfo.restoreConfig.manifest : snapshotInfo.manifest; // compat
|
|
const remotePath = `${tag}/app_${app.fqdn}_v${manifest.version}`;
|
|
const format = backupConfig.format;
|
|
|
|
debug(`rotateAppBackup: rotating ${app.fqdn} to path ${remotePath}`);
|
|
|
|
const data = {
|
|
remotePath,
|
|
encryptionVersion: backupConfig.encryption ? 2 : null,
|
|
packageVersion: manifest.version,
|
|
type: backups.BACKUP_TYPE_APP,
|
|
state: backups.BACKUP_STATE_CREATING,
|
|
identifier: app.id,
|
|
dependsOn: [],
|
|
manifest,
|
|
format,
|
|
preserveSecs: options.preserveSecs || 0
|
|
};
|
|
|
|
const id = await backups.add(data);
|
|
const [error] = await safe(copy(backupConfig, `snapshot/app_${app.id}`, remotePath, progressCallback));
|
|
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
|
|
await backups.setState(id, state);
|
|
if (error) throw error;
|
|
|
|
return id;
|
|
}
|
|
|
|
async function backupApp(app, options, progressCallback) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
if (options.snapshotOnly) return await snapshotApp(app, progressCallback);
|
|
|
|
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
|
|
|
debug(`backupApp: backing up ${app.fqdn} with tag ${tag}`);
|
|
|
|
return await backupAppWithTag(app, tag, options, progressCallback);
|
|
}
|
|
|
|
async function snapshotApp(app, progressCallback) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const startTime = new Date();
|
|
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
|
|
|
|
await apps.writeConfig(app);
|
|
await services.backupAddons(app, app.manifest.addons);
|
|
|
|
debug(`snapshotApp: ${app.fqdn} took ${(new Date() - startTime)/1000} seconds`);
|
|
}
|
|
|
|
async function uploadAppSnapshot(backupConfig, app, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
await snapshotApp(app, progressCallback);
|
|
|
|
const remotePath = `snapshot/app_${app.id}`;
|
|
const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id));
|
|
if (!appDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving appsdata: ${safe.error.message}`);
|
|
|
|
const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []);
|
|
|
|
progressCallback({ message: `Uploading app snapshot ${app.fqdn}`});
|
|
|
|
const uploadConfig = {
|
|
remotePath,
|
|
backupConfig,
|
|
dataLayout,
|
|
progressTag: app.fqdn
|
|
};
|
|
|
|
const startTime = new Date();
|
|
|
|
await runBackupUpload(uploadConfig, progressCallback);
|
|
|
|
debug(`uploadAppSnapshot: ${app.fqdn} uploaded to ${remotePath}. ${(new Date() - startTime)/1000} seconds`);
|
|
|
|
await backups.setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format });
|
|
}
|
|
|
|
async function backupAppWithTag(app, tag, options, progressCallback) {
|
|
assert.strictEqual(typeof app, 'object');
|
|
assert.strictEqual(typeof tag, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
if (!canBackupApp(app)) { // if we cannot backup, reuse it's most recent backup
|
|
const results = await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1);
|
|
if (results.length === 0) return null; // no backup to re-use
|
|
|
|
return results[0].id;
|
|
}
|
|
|
|
const backupConfig = await backups.getConfig();
|
|
|
|
await uploadAppSnapshot(backupConfig, app, progressCallback);
|
|
return await rotateAppBackup(backupConfig, app, tag, options, progressCallback);
|
|
}
|
|
|
|
async function uploadMailSnapshot(backupConfig, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const mailDataDir = safe.fs.realpathSync(paths.MAIL_DATA_DIR);
|
|
if (!mailDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving maildata: ${safe.error.message}`);
|
|
|
|
const uploadConfig = {
|
|
remotePath: 'snapshot/mail',
|
|
backupConfig,
|
|
dataLayout: new DataLayout(mailDataDir, []),
|
|
progressTag: 'mail'
|
|
};
|
|
|
|
progressCallback({ message: 'Uploading mail snapshot' });
|
|
|
|
const startTime = new Date();
|
|
|
|
await runBackupUpload(uploadConfig, progressCallback);
|
|
|
|
debug(`uploadMailSnapshot: took ${(new Date() - startTime)/1000} seconds`);
|
|
|
|
await backups.setSnapshotInfo('mail', { timestamp: new Date().toISOString(), format: backupConfig.format });
|
|
}
|
|
|
|
async function rotateMailBackup(backupConfig, tag, options, progressCallback) {
|
|
assert.strictEqual(typeof backupConfig, 'object');
|
|
assert.strictEqual(typeof tag, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const remotePath = `${tag}/mail_v${constants.VERSION}`;
|
|
const format = backupConfig.format;
|
|
|
|
debug(`rotateMailBackup: rotating to ${remotePath}`);
|
|
|
|
const data = {
|
|
remotePath,
|
|
encryptionVersion: backupConfig.encryption ? 2 : null,
|
|
packageVersion: constants.VERSION,
|
|
type: backups.BACKUP_TYPE_MAIL,
|
|
state: backups.BACKUP_STATE_CREATING,
|
|
identifier: backups.BACKUP_IDENTIFIER_MAIL,
|
|
dependsOn: [],
|
|
manifest: null,
|
|
format,
|
|
preserveSecs: options.preserveSecs || 0
|
|
};
|
|
|
|
const id = await backups.add(data);
|
|
const [error] = await safe(copy(backupConfig, 'snapshot/mail', remotePath, progressCallback));
|
|
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
|
|
await backups.setState(id, state);
|
|
if (error) throw error;
|
|
|
|
return id;
|
|
}
|
|
|
|
async function backupMailWithTag(tag, options, progressCallback) {
|
|
assert.strictEqual(typeof tag, 'string');
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
debug(`backupMailWithTag: backing up mail with tag ${tag}`);
|
|
|
|
const backupConfig = await backups.getConfig();
|
|
await uploadMailSnapshot(backupConfig, progressCallback);
|
|
return await rotateMailBackup(backupConfig, tag, options, progressCallback);
|
|
}
|
|
|
|
async function backupMail(options, progressCallback) {
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
|
|
debug(`backupMail: backing up mail with tag ${tag}`);
|
|
return await backupMailWithTag(tag, options, progressCallback);
|
|
}
|
|
|
|
async function downloadMail(restoreConfig, progressCallback) {
|
|
assert.strictEqual(typeof restoreConfig, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const mailDataDir = safe.fs.realpathSync(paths.MAIL_DATA_DIR);
|
|
if (!mailDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving maildata: ${safe.error.message}`);
|
|
const dataLayout = new DataLayout(mailDataDir, []);
|
|
|
|
const startTime = new Date();
|
|
|
|
await download(restoreConfig.backupConfig, restoreConfig.remotePath, restoreConfig.backupFormat, dataLayout, progressCallback);
|
|
debug('downloadMail: time: %s', (new Date() - startTime)/1000);
|
|
}
|
|
|
|
// this function is called from external process. calling process is expected to have a lock
|
|
async function fullBackup(options, progressCallback) {
|
|
assert.strictEqual(typeof options, 'object');
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
|
|
|
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); // unique tag under which all apps/mail/box backs up
|
|
|
|
const allApps = await apps.list();
|
|
|
|
let percent = 1;
|
|
const step = 100/(allApps.length+3);
|
|
|
|
const appBackupIds = [];
|
|
for (let i = 0; i < allApps.length; i++) {
|
|
const app = allApps[i];
|
|
progressCallback({ percent: percent, message: `Backing up ${app.fqdn} (${i+1}/${allApps.length})` });
|
|
percent += step;
|
|
|
|
if (!app.enableBackup) {
|
|
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length}) since automatic backup disabled`);
|
|
continue; // nothing to backup
|
|
}
|
|
|
|
const startTime = new Date();
|
|
const appBackupId = await backupAppWithTag(app, tag, options, (progress) => progressCallback({ percent, message: progress.message }));
|
|
debug(`fullBackup: app ${app.fqdn} backup finished. Took ${(new Date() - startTime)/1000} seconds`);
|
|
if (appBackupId) appBackupIds.push(appBackupId); // backupId can be null if in BAD_STATE and never backed up
|
|
}
|
|
|
|
progressCallback({ percent, message: 'Backing up mail' });
|
|
percent += step;
|
|
const mailBackupId = await backupMailWithTag(tag, options, (progress) => progressCallback({ percent, message: progress.message }));
|
|
|
|
progressCallback({ percent, message: 'Backing up system data' });
|
|
percent += step;
|
|
|
|
const dependsOn = appBackupIds.concat(mailBackupId);
|
|
const backupId = await backupBox(dependsOn, tag, options, (progress) => progressCallback({ percent, message: progress.message }));
|
|
return backupId;
|
|
}
|