Files
cloudron-box/src/backuptask.js

532 lines
22 KiB
JavaScript
Raw Normal View History

2021-07-14 11:07:19 -07:00
'use strict';
exports = module.exports = {
fullBackup,
appBackup,
2021-07-14 11:07:19 -07:00
restore,
downloadApp,
backupApp,
2021-07-14 11:07:19 -07:00
downloadMail,
2021-07-14 11:07:19 -07:00
upload,
};
const apps = require('./apps.js'),
assert = require('assert'),
backupFormat = require('./backupformat.js'),
backups = require('./backups.js'),
2025-07-24 18:46:21 +02:00
backupTargets = require('./backuptargets.js'),
2021-07-14 11:07:19 -07:00
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'),
remove global lock Currently, the update/apptask/fullbackup/platformstart take a global lock and cannot run in parallel. This causes situations where when a user tries to trigger an apptask, it says "waiting for backup to finish..." etc The solution is to let them run in parallel. We need a lock at the app level as app operations running in parallel would be bad (tm). In addition, the update task needs a lock just for the update part. We also need multi-process locks. Running tasks as processes is core to our "kill" strategy. Various inter process locks were explored: * node's IPC mechanism with process.send(). But this only works for direct node.js children. taskworker is run via sudo and the IPC does not work. * File lock using O_EXCL. Basic ideas to create lock files. While file creation can be done atomically, it becomes complicated to clean up lock files when the tasks crash. We need a way to know what locks were held by the crashing task. flock and friends are not built-into node.js * sqlite/redis were options but introduce additional deps * Settled on MySQL based locking. Initial plan was to have row locks or table locks. Each row is a kind of lock. While implementing, it was found that we need many types of locks (and not just update lock and app locks). For example, we need locks for each task type, so that only one task type is active at a time. * Instead of rows, we can just lock table and have a json blob in it. This hit a road block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e when issing two db.query() it might use two different connections from the pool. We have to expose the connection, release connection etc. * Next idea was atomic blob update of the blob checking if old blob was same. This approach, was finally refined into a version field. Phew!
2024-12-07 14:35:45 +01:00
locks = require('./locks.js'),
2021-07-14 11:07:19 -07:00
path = require('path'),
paths = require('./paths.js'),
safe = require('safetydance'),
services = require('./services.js'),
shell = require('./shell.js')('backuptask');
2021-07-14 11:07:19 -07:00
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
2025-07-24 19:02:02 +02:00
async function checkPreconditions(backupTarget, dataLayout) {
assert.strictEqual(typeof backupTarget, 'object');
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
// check mount status before uploading
2025-07-24 19:02:02 +02:00
const status = await backupTargets.ensureMounted(backupTarget);
debug(`checkPreconditions: mount point status is ${JSON.stringify(status)}`);
2022-11-05 08:43:02 +01:00
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 backupTargets.storageApi(backupTarget).getAvailableSize(backupTarget.config);
let used = 0;
for (const localPath of dataLayout.localPaths()) {
debug(`checkPreconditions: getting disk usage of ${localPath}`);
// du can error when files go missing as it is computing the size. it still prints some size anyway
// to match df output in getAvailableSize() we must use disk usage size here and not apparent size
const [duError, result] = await safe(shell.spawn('du', [ '--dereference-args', '--summarize', '--block-size=1', '--exclude=*.lock', '--exclude=dovecot.list.index.log.*', localPath], { encoding: 'utf8' }));
if (duError) debug(`checkPreconditions: du error for ${localPath}. code: ${duError.code} stderror: ${duError.stderr}`);
used += parseInt(duError ? duError.stdout : 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)}`);
}
2021-07-14 11:07:19 -07:00
// this function is called via backupupload (since it needs root to traverse app's directory)
2025-07-24 19:02:02 +02:00
async function upload(remotePath, targetId, dataLayoutString, progressCallback) {
assert.strictEqual(typeof remotePath, 'string');
2025-07-24 19:02:02 +02:00
assert.strictEqual(typeof targetId, 'string');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof dataLayoutString, 'string');
assert.strictEqual(typeof progressCallback, 'function');
2025-07-24 19:02:02 +02:00
debug(`upload: path ${remotePath} target ${targetId} dataLayout ${dataLayoutString}`);
const backupTarget = await backupTargets.get(targetId);
if (!backupTarget) throw new BoxError(BoxError.NOT_FOUND, 'Backup target not found');
2021-07-14 11:07:19 -07:00
const dataLayout = DataLayout.fromString(dataLayoutString);
2025-07-24 19:02:02 +02:00
await checkPreconditions(backupTarget, dataLayout);
2021-07-14 11:07:19 -07:00
2025-07-24 19:02:02 +02:00
await backupFormat.api(backupTarget.format).upload(backupTarget, remotePath, dataLayout, progressCallback);
2021-07-14 11:07:19 -07:00
}
2025-07-25 12:55:14 +02:00
async function download(backupTarget, remotePath, dataLayout, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof remotePath, 'string');
2021-07-14 11:07:19 -07:00
assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout');
assert.strictEqual(typeof progressCallback, 'function');
2025-07-25 12:55:14 +02:00
debug(`download: Downloading ${remotePath} of format ${backupTarget.format} (encrypted: ${!!backupTarget.encryption}) to ${dataLayout.toString()}`);
2021-07-14 11:07:19 -07:00
2025-07-25 12:55:14 +02:00
await backupFormat.api(backupTarget.format).download(backupTarget, remotePath, dataLayout, progressCallback);
2021-07-14 11:07:19 -07:00
}
async function restore(backupConfig, remotePath, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof backupConfig, 'object');
assert.strictEqual(typeof remotePath, 'string');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof progressCallback, 'function');
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
2021-09-16 13:59:03 -07:00
if (!boxDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`);
2021-07-14 11:07:19 -07:00
const dataLayout = new DataLayout(boxDataDir, []);
2022-04-28 21:29:11 -07:00
await download(backupConfig, remotePath, backupConfig.format, dataLayout, progressCallback);
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
debug('restore: download completed, importing database');
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
await database.importFromFile(`${dataLayout.localRoot()}/box.mysqldump`);
debug('restore: database imported');
remove global lock Currently, the update/apptask/fullbackup/platformstart take a global lock and cannot run in parallel. This causes situations where when a user tries to trigger an apptask, it says "waiting for backup to finish..." etc The solution is to let them run in parallel. We need a lock at the app level as app operations running in parallel would be bad (tm). In addition, the update task needs a lock just for the update part. We also need multi-process locks. Running tasks as processes is core to our "kill" strategy. Various inter process locks were explored: * node's IPC mechanism with process.send(). But this only works for direct node.js children. taskworker is run via sudo and the IPC does not work. * File lock using O_EXCL. Basic ideas to create lock files. While file creation can be done atomically, it becomes complicated to clean up lock files when the tasks crash. We need a way to know what locks were held by the crashing task. flock and friends are not built-into node.js * sqlite/redis were options but introduce additional deps * Settled on MySQL based locking. Initial plan was to have row locks or table locks. Each row is a kind of lock. While implementing, it was found that we need many types of locks (and not just update lock and app locks). For example, we need locks for each task type, so that only one task type is active at a time. * Instead of rows, we can just lock table and have a json blob in it. This hit a road block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e when issing two db.query() it might use two different connections from the pool. We have to expose the connection, release connection etc. * Next idea was atomic blob update of the blob checking if old blob was same. This approach, was finally refined into a version field. Phew!
2024-12-07 14:35:45 +01:00
2025-07-14 15:01:30 +02:00
await locks.releaseAll(); // clear the locks table in database
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function downloadApp(app, restoreConfig, progressCallback) {
2021-07-14 11:07:19 -07:00
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));
2021-09-16 13:59:03 -07:00
if (!appDataDir) throw new BoxError(BoxError.FS_ERROR, safe.error.message);
2022-06-01 22:44:52 -07:00
const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []);
2021-07-14 11:07:19 -07:00
const startTime = new Date();
2025-07-25 12:55:14 +02:00
const backup = await backups.get(restoreConfig.backupId);
const backupTarget = await backupTargets.get(backup.targetId);
2021-07-14 11:07:19 -07:00
2025-07-25 12:55:14 +02:00
await download(backupTarget, backup.remotePath, dataLayout, progressCallback);
2021-09-16 13:59:03 -07:00
debug('downloadApp: time: %s', (new Date() - startTime)/1000);
2021-07-14 11:07:19 -07:00
}
2022-04-28 21:29:11 -07:00
async function runBackupUpload(uploadConfig, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof uploadConfig, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2025-07-24 19:02:02 +02:00
const { remotePath, backupTarget, dataLayout, progressTag } = uploadConfig;
assert.strictEqual(typeof remotePath, 'string');
2025-07-24 19:02:02 +02:00
assert.strictEqual(typeof backupTarget, 'object');
2021-07-14 11:07:19 -07:00
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);
2025-07-24 19:02:02 +02:00
if (backupTarget.limits?.memoryLimit >= 2*1024*1024*1024) {
const heapSize = Math.min((backupTarget.limits.memoryLimit/1024/1024) - 256, 8192);
2021-07-14 11:07:19 -07:00
debug(`runBackupUpload: adjusting heap size to ${heapSize}M`);
envCopy.NODE_OPTIONS = `--max-old-space-size=${heapSize}`;
}
2022-04-28 21:29:11 -07:00
let result = ''; // the script communicates error result as a string
function onMessage(progress) { // this is { message } or { result }
2021-07-14 11:07:19 -07:00
if ('message' in progress) return progressCallback({ message: `${progress.message} (${progressTag})` });
debug(`runBackupUpload: result - ${JSON.stringify(progress)}`);
result = progress.result;
2022-04-28 21:29:11 -07:00
}
2025-07-16 21:32:27 +02:00
// do not use debug for logging child output because it already has timestamps via it's own debug
2025-07-24 19:02:02 +02:00
const [error] = await safe(shell.sudo([ BACKUP_UPLOAD_CMD, remotePath, backupTarget.id, dataLayout.toString() ], { env: envCopy, preserveEnv: true, onMessage, logger: process.stdout.write }));
2022-04-28 21:29:11 -07:00
if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed
2024-10-14 18:26:01 +02:00
debug(`runBackupUpload: backuptask crashed`, error);
2022-04-28 21:29:11 -07:00
throw new BoxError(BoxError.INTERNAL_ERROR, 'Backuptask crashed');
} else if (error && error.code === 50) { // exited with error
throw new BoxError(BoxError.EXTERNAL_ERROR, result);
}
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function snapshotBox(progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof progressCallback, 'function');
progressCallback({ message: 'Snapshotting box' });
const startTime = new Date();
2021-09-16 13:59:03 -07:00
await database.exportToFile(`${paths.BOX_DATA_DIR}/box.mysqldump`);
debug(`snapshotBox: took ${(new Date() - startTime)/1000} seconds`);
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function uploadBoxSnapshot(backupTarget, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof progressCallback, 'function');
2021-09-16 13:59:03 -07:00
await snapshotBox(progressCallback);
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR);
if (!boxDataDir) throw new BoxError(BoxError.FS_ERROR, `Error resolving boxdata: ${safe.error.message}`);
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const uploadConfig = {
remotePath: 'snapshot/box',
2025-07-24 19:02:02 +02:00
backupTarget,
2021-09-16 13:59:03 -07:00
dataLayout: new DataLayout(boxDataDir, []),
progressTag: 'box'
};
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
progressCallback({ message: 'Uploading box snapshot' });
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const startTime = new Date();
2021-07-14 11:07:19 -07:00
2022-04-28 21:29:11 -07:00
await runBackupUpload(uploadConfig, progressCallback);
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
debug(`uploadBoxSnapshot: took ${(new Date() - startTime)/1000} seconds`);
2021-07-14 11:07:19 -07:00
await backupTargets.setSnapshotInfo(backupTarget, 'box', { timestamp: new Date().toISOString() });
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function copy(backupTarget, srcRemotePath, destRemotePath, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof srcRemotePath, 'string');
assert.strictEqual(typeof destRemotePath, 'string');
assert.strictEqual(typeof progressCallback, 'function');
const oldFilePath = backupTargets.getBackupFilePath(backupTarget, srcRemotePath);
const newFilePath = backupTargets.getBackupFilePath(backupTarget, destRemotePath);
2022-04-30 16:01:42 -07:00
const startTime = new Date();
const [copyError] = await safe(backupTargets.storageApi(backupTarget).copy(backupTarget.config, oldFilePath, newFilePath, progressCallback));
2023-01-17 10:43:17 +01:00
if (copyError) {
debug(`copy: copied to ${destRemotePath} errored. error: ${copyError.message}`);
throw copyError;
}
2022-04-30 16:01:42 -07:00
debug(`copy: copied successfully to ${destRemotePath}. Took ${(new Date() - startTime)/1000} seconds`);
}
2025-07-24 19:02:02 +02:00
async function rotateBoxBackup(backupTarget, tag, options, dependsOn, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert(Array.isArray(dependsOn));
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof progressCallback, 'function');
const remotePath = `${tag}/box_v${constants.VERSION}`;
2021-07-14 11:07:19 -07:00
debug(`rotateBoxBackup: rotating to id ${remotePath}`);
2021-07-14 11:07:19 -07:00
const data = {
remotePath,
2025-07-24 19:02:02 +02:00
encryptionVersion: backupTarget.encryption ? 2 : null,
2021-07-14 11:07:19 -07:00
packageVersion: constants.VERSION,
type: backups.BACKUP_TYPE_BOX,
state: backups.BACKUP_STATE_CREATING,
identifier: backups.BACKUP_IDENTIFIER_BOX,
dependsOn,
2021-07-14 11:07:19 -07:00
manifest: null,
preserveSecs: options.preserveSecs || 0,
2025-07-25 07:44:25 +02:00
appConfig: null,
targetId: backupTarget.id
2021-07-14 11:07:19 -07:00
};
const id = await backups.add(data);
2025-07-24 19:02:02 +02:00
const [error] = await safe(copy(backupTarget, 'snapshot/box', remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
2022-04-05 13:11:30 +02:00
if (error) throw error;
return id;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function backupBox(backupTarget, dependsOn, tag, options, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert(Array.isArray(dependsOn));
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2025-07-24 19:02:02 +02:00
await uploadBoxSnapshot(backupTarget, progressCallback);
return await rotateBoxBackup(backupTarget, tag, options, dependsOn, progressCallback);
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function rotateAppBackup(backupTarget, app, tag, options, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const manifest = app.manifest;
const remotePath = `${tag}/app_${app.fqdn}_v${manifest.version}`;
2021-07-14 11:07:19 -07:00
debug(`rotateAppBackup: rotating ${app.fqdn} to path ${remotePath}`);
2021-07-14 11:07:19 -07:00
const data = {
remotePath,
2025-07-24 19:02:02 +02:00
encryptionVersion: backupTarget.encryption ? 2 : null,
2021-07-14 11:07:19 -07:00
packageVersion: manifest.version,
type: backups.BACKUP_TYPE_APP,
state: backups.BACKUP_STATE_CREATING,
2021-07-14 11:07:19 -07:00
identifier: app.id,
dependsOn: [],
2021-07-14 11:07:19 -07:00
manifest,
preserveSecs: options.preserveSecs || 0,
2025-07-25 07:44:25 +02:00
appConfig: app,
targetId: backupTarget.id
2021-07-14 11:07:19 -07:00
};
const id = await backups.add(data);
2025-07-24 19:02:02 +02:00
const [error] = await safe(copy(backupTarget, `snapshot/app_${app.id}`, remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
2022-04-05 13:11:30 +02:00
if (error) throw error;
return id;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function backupApp(app, backupTarget, options, progressCallback) {
assert.strictEqual(typeof app, 'object');
2025-07-24 19:02:02 +02:00
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
let backupId = null;
await locks.wait(`${locks.TYPE_APP_BACKUP_PREFIX}${app.id}`);
if (options.snapshotOnly) {
await snapshotApp(app, progressCallback);
} else {
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
2025-07-24 19:02:02 +02:00
backupId = await backupAppWithTag(app, backupTarget, tag, options, progressCallback);
}
await locks.release(`${locks.TYPE_APP_BACKUP_PREFIX}${app.id}`);
return backupId;
}
2021-09-16 13:59:03 -07:00
async function snapshotApp(app, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const startTime = new Date();
progressCallback({ message: `Snapshotting app ${app.fqdn}` });
2024-02-10 11:53:25 +01:00
await apps.writeConfig(app);
2021-09-16 13:59:03 -07:00
await services.backupAddons(app, app.manifest.addons);
2021-07-14 11:07:19 -07:00
2021-09-26 18:45:23 -07:00
debug(`snapshotApp: ${app.fqdn} took ${(new Date() - startTime)/1000} seconds`);
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function uploadAppSnapshot(backupTarget, app, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2021-09-16 13:59:03 -07:00
await snapshotApp(app, progressCallback);
2021-07-14 11:07:19 -07:00
2022-04-26 18:53:07 -07:00
const remotePath = `snapshot/app_${app.id}`;
2021-09-16 13:59:03 -07:00
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}`);
2021-07-14 11:07:19 -07:00
2022-06-01 22:44:52 -07:00
const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []);
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
progressCallback({ message: `Uploading app snapshot ${app.fqdn}`});
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const uploadConfig = {
remotePath,
2025-07-24 19:02:02 +02:00
backupTarget,
2021-09-16 13:59:03 -07:00
dataLayout,
progressTag: app.fqdn
};
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const startTime = new Date();
2021-07-14 11:07:19 -07:00
2022-04-28 21:29:11 -07:00
await runBackupUpload(uploadConfig, progressCallback);
2021-07-14 11:07:19 -07:00
2022-04-26 18:53:07 -07:00
debug(`uploadAppSnapshot: ${app.fqdn} uploaded to ${remotePath}. ${(new Date() - startTime)/1000} seconds`);
2021-07-14 11:07:19 -07:00
await backupTargets.setSnapshotInfo(backupTarget, app.id, { timestamp: new Date().toISOString(), manifest: app.manifest });
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function backupAppWithTag(app, backupTarget, tag, options, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof app, 'object');
2025-07-24 19:02:02 +02:00
assert.strictEqual(typeof backupTarget, 'object');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
if (!apps.canBackupApp(app)) { // if we cannot backup, reuse it's most recent backup
const lastKnownGoodAppBackup = await backups.getLatestInTargetByIdentifier(app.id, backupTarget.id);
if (lastKnownGoodAppBackup === null) return null; // no backup to re-use
return lastKnownGoodAppBackup.id;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
await uploadAppSnapshot(backupTarget, app, progressCallback);
return await rotateAppBackup(backupTarget, app, tag, options, progressCallback);
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function uploadMailSnapshot(backupTarget, progressCallback) {
assert.strictEqual(typeof backupTarget, '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',
2025-07-24 19:02:02 +02:00
backupTarget,
dataLayout: new DataLayout(mailDataDir, []),
progressTag: 'mail'
};
progressCallback({ message: 'Uploading mail snapshot' });
const startTime = new Date();
2022-04-28 21:29:11 -07:00
await runBackupUpload(uploadConfig, progressCallback);
debug(`uploadMailSnapshot: took ${(new Date() - startTime)/1000} seconds`);
await backupTargets.setSnapshotInfo(backupTarget, 'mail', { timestamp: new Date().toISOString() });
}
2025-07-25 07:44:25 +02:00
async function rotateMailBackup(backupTarget, tag, options, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const remotePath = `${tag}/mail_v${constants.VERSION}`;
debug(`rotateMailBackup: rotating to ${remotePath}`);
const data = {
remotePath,
2025-07-25 07:44:25 +02:00
encryptionVersion: backupTarget.encryption ? 2 : null,
packageVersion: constants.VERSION,
type: backups.BACKUP_TYPE_MAIL,
state: backups.BACKUP_STATE_CREATING,
identifier: backups.BACKUP_IDENTIFIER_MAIL,
dependsOn: [],
manifest: null,
preserveSecs: options.preserveSecs || 0,
2025-07-25 07:44:25 +02:00
appConfig: null,
targetId: backupTarget.id
};
const id = await backups.add(data);
2025-07-25 07:44:25 +02:00
const [error] = await safe(copy(backupTarget, 'snapshot/mail', remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
2022-04-05 13:11:30 +02:00
if (error) throw error;
return id;
}
2025-07-25 07:44:25 +02:00
async function backupMailWithTag(backupTarget, tag, options, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof tag, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
debug(`backupMailWithTag: backing up mail with tag ${tag}`);
2025-07-25 07:44:25 +02:00
await uploadMailSnapshot(backupTarget, progressCallback);
return await rotateMailBackup(backupTarget, 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();
2022-04-28 21:29:11 -07:00
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
2025-07-24 19:02:02 +02:00
async function fullBackup(backupTargetId, options, progressCallback) {
assert.strictEqual(typeof backupTargetId, 'string');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2025-07-24 19:02:02 +02:00
const backupTarget = await backupTargets.get(backupTargetId);
if (!backupTarget) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Backup target not found');
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,''); // unique tag under which all apps/mail/box backs up
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const allApps = await apps.list();
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
let percent = 1;
2024-07-08 10:47:00 +02:00
const step = 100/(allApps.length+3);
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const appBackupIds = [];
2021-11-02 18:07:19 -07:00
for (let i = 0; i < allApps.length; i++) {
const app = allApps[i];
2021-09-16 13:59:03 -07:00
percent += step;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
if (!app.enableBackup) {
2022-02-28 11:04:44 -08:00
debug(`fullBackup: skipped backup ${app.fqdn} (${i+1}/${allApps.length}) since automatic backup disabled`);
continue; // nothing to backup
2021-09-16 13:59:03 -07:00
}
2021-07-14 11:07:19 -07:00
2024-12-09 08:38:23 +01:00
progressCallback({ percent, message: `Backing up ${app.fqdn} (${i+1}/${allApps.length}). Waiting for lock` });
await locks.wait(`${locks.TYPE_APP_BACKUP_PREFIX}${app.id}`);
2021-09-16 13:59:03 -07:00
const startTime = new Date();
2025-07-24 19:02:02 +02:00
const [appBackupError, appBackupId] = await safe(backupAppWithTag(app, backupTarget, tag, options, (progress) => progressCallback({ percent, message: progress.message })));
debug(`fullBackup: app ${app.fqdn} backup finished. Took ${(new Date() - startTime)/1000} seconds`);
await locks.release(`${locks.TYPE_APP_BACKUP_PREFIX}${app.id}`);
2024-12-17 19:08:43 +01:00
if (appBackupError) throw appBackupError;
if (appBackupId) appBackupIds.push(appBackupId); // backupId can be null if in BAD_STATE and never backed up
2021-09-16 13:59:03 -07:00
}
2021-07-14 11:07:19 -07:00
progressCallback({ percent, message: 'Backing up mail' });
percent += step;
2025-07-24 19:02:02 +02:00
const mailBackupId = await backupMailWithTag(backupTarget, tag, options, (progress) => progressCallback({ percent, message: progress.message }));
progressCallback({ percent, message: 'Backing up system data' });
2021-09-16 13:59:03 -07:00
percent += step;
2021-07-14 11:07:19 -07:00
const dependsOn = appBackupIds.concat(mailBackupId);
2025-07-24 19:02:02 +02:00
const backupId = await backupBox(backupTarget, dependsOn, tag, options, (progress) => progressCallback({ percent, message: progress.message }));
2021-09-16 13:59:03 -07:00
return backupId;
2021-07-14 11:07:19 -07:00
}
// this function is called from external process
2025-07-24 19:02:02 +02:00
async function appBackup(appId, backupTargetId, options, progressCallback) {
assert.strictEqual(typeof appId, 'string');
2025-07-25 12:55:14 +02:00
assert.strictEqual(typeof backupTargetId, 'string');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
const app = await apps.get(appId);
if (!app) throw new BoxError(BoxError.BAD_FIELD, 'App not found');
2025-07-24 19:02:02 +02:00
const backupTarget = await backupTargets.get(backupTargetId);
if (!backupTarget) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Backup target not found');
await progressCallback({ percent: 1, message: `Backing up ${app.fqdn}. Waiting for lock` });
const startTime = new Date();
2025-07-24 19:02:02 +02:00
const backupId = await backupApp(app, backupTarget, options, progressCallback);
await progressCallback({ percent: 100, message: `app ${app.fqdn} backup finished. Took ${(new Date() - startTime)/1000} seconds` });
return backupId;
}