Files
cloudron-box/src/backuptask.js

531 lines
22 KiB
JavaScript
Raw Normal View History

2021-07-14 11:07:19 -07:00
'use strict';
exports = module.exports = {
fullBackup,
2021-07-14 11:07:19 -07:00
restore,
backupApp,
downloadApp,
backupMail,
downloadMail,
2021-07-14 11:07:19 -07:00
upload,
};
const apps = require('./apps.js'),
assert = require('assert'),
backupFormat = require('./backupformat.js'),
2021-07-14 11:07:19 -07:00
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'),
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'),
2024-10-14 19:10:31 +02:00
shell = require('./shell.js')('backuptask'),
2022-04-30 16:42:14 -07:00
storage = require('./storage.js');
2021-07-14 11:07:19 -07:00
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(`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 storage.api(backupConfig.provider).getAvailableSize(backupConfig);
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)
2022-04-28 21:29:11 -07:00
async function upload(remotePath, format, dataLayoutString, progressCallback) {
assert.strictEqual(typeof remotePath, 'string');
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof format, 'string');
assert.strictEqual(typeof dataLayoutString, 'string');
assert.strictEqual(typeof progressCallback, 'function');
debug(`upload: path ${remotePath} format ${format} dataLayout ${dataLayoutString}`);
2021-07-14 11:07:19 -07:00
const dataLayout = DataLayout.fromString(dataLayoutString);
2023-08-04 11:24:28 +05:30
const backupConfig = await backups.getConfig();
await checkPreconditions(backupConfig, dataLayout);
2021-07-14 11:07:19 -07:00
2022-04-30 16:42:14 -07:00
await backupFormat.api(format).upload(backupConfig, remotePath, dataLayout, progressCallback);
2021-07-14 11:07:19 -07:00
}
2022-04-28 21:29:11 -07:00
async function download(backupConfig, remotePath, format, dataLayout, 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 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()}`);
2021-07-14 11:07:19 -07:00
2022-04-30 16:42:14 -07:00
await backupFormat.api(format).download(backupConfig, 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
await locks.releaseAll();
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();
2023-08-04 11:24:28 +05:30
const backupConfig = restoreConfig.backupConfig || await backups.getConfig();
2021-07-14 11:07:19 -07:00
2022-04-28 21:29:11 -07:00
await download(backupConfig, restoreConfig.remotePath, restoreConfig.backupFormat, 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');
const { remotePath, backupConfig, dataLayout, progressTag } = uploadConfig;
assert.strictEqual(typeof remotePath, 'string');
2021-07-14 11:07:19 -07:00
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);
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
}
2024-10-14 19:10:31 +02:00
const [error] = await safe(shell.promises.sudo([ BACKUP_UPLOAD_CMD, remotePath, backupConfig.format, dataLayout.toString() ], { env: envCopy, preserveEnv: true, ipc: true, onMessage, outputHasTimestamps: true }));
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
}
2021-09-16 13:59:03 -07:00
async function uploadBoxSnapshot(backupConfig, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof backupConfig, 'object');
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',
2021-09-16 13:59:03 -07:00
backupConfig,
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
2021-09-16 13:59:03 -07:00
await backups.setSnapshotInfo('box', { timestamp: new Date().toISOString(), format: backupConfig.format });
2021-07-14 11:07:19 -07:00
}
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);
2022-04-30 16:01:42 -07:00
const startTime = new Date();
2023-01-17 10:43:17 +01:00
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;
}
2022-04-30 16:01:42 -07:00
debug(`copy: copied successfully to ${destRemotePath}. Took ${(new Date() - startTime)/1000} seconds`);
}
async function rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof backupConfig, 'object');
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
const format = backupConfig.format;
debug(`rotateBoxBackup: rotating to id ${remotePath}`);
2021-07-14 11:07:19 -07:00
const data = {
remotePath,
2021-07-14 11:07:19 -07:00
encryptionVersion: backupConfig.encryption ? 2 : null,
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,
format,
preserveSecs: options.preserveSecs || 0,
appConfig: null
2021-07-14 11:07:19 -07:00
};
const id = await backups.add(data);
2022-04-05 13:11:30 +02:00
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);
2022-04-05 13:11:30 +02:00
if (error) throw error;
return id;
2021-07-14 11:07:19 -07:00
}
async function backupBox(dependsOn, tag, options, progressCallback) {
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');
2023-08-04 11:24:28 +05:30
const backupConfig = await backups.getConfig();
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
await uploadBoxSnapshot(backupConfig, progressCallback);
return await rotateBoxBackup(backupConfig, tag, options, dependsOn, progressCallback);
2021-07-14 11:07:19 -07:00
}
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}`;
2021-07-14 11:07:19 -07:00
const format = backupConfig.format;
debug(`rotateAppBackup: rotating ${app.fqdn} to path ${remotePath}`);
2021-07-14 11:07:19 -07:00
const data = {
remotePath,
2021-07-14 11:07:19 -07:00
encryptionVersion: backupConfig.encryption ? 2 : null,
packageVersion: manifest.version,
type: backups.BACKUP_TYPE_APP,
state: backups.BACKUP_STATE_CREATING,
identifier: app.id,
dependsOn: [],
2021-07-14 11:07:19 -07:00
manifest,
format,
preserveSecs: options.preserveSecs || 0,
appConfig: app
2021-07-14 11:07:19 -07:00
};
const id = await backups.add(data);
2022-04-05 13:11:30 +02:00
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);
2022-04-05 13:11:30 +02:00
if (error) throw error;
return id;
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function backupApp(app, options, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof app, 'object');
assert.strictEqual(typeof options, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2021-09-16 13:59:03 -07:00
if (options.snapshotOnly) return await snapshotApp(app, progressCallback);
2021-07-14 11:07:19 -07:00
const tag = (new Date()).toISOString().replace(/[T.]/g, '-').replace(/[:Z]/g,'');
2021-09-26 18:45:23 -07:00
debug(`backupApp: backing up ${app.fqdn} with tag ${tag}`);
2021-07-14 11:07:19 -07:00
return await backupAppWithTag(app, tag, options, progressCallback);
2021-07-14 11:07:19 -07:00
}
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
}
2021-09-16 13:59:03 -07:00
async function uploadAppSnapshot(backupConfig, app, progressCallback) {
2021-07-14 11:07:19 -07:00
assert.strictEqual(typeof backupConfig, 'object');
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,
2021-09-16 13:59:03 -07:00
backupConfig,
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
2021-09-16 13:59:03 -07:00
await backups.setSnapshotInfo(app.id, { timestamp: new Date().toISOString(), manifest: app.manifest, format: backupConfig.format });
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function backupAppWithTag(app, tag, options, progressCallback) {
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');
if (!canBackupApp(app)) { // if we cannot backup, reuse it's most recent backup
2021-09-16 13:59:03 -07:00
const results = await backups.getByIdentifierAndStatePaged(app.id, backups.BACKUP_STATE_NORMAL, 1, 1);
if (results.length === 0) return null; // no backup to re-use
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
return results[0].id;
2021-07-14 11:07:19 -07:00
}
2023-08-04 11:24:28 +05:30
const backupConfig = await backups.getConfig();
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
await uploadAppSnapshot(backupConfig, app, progressCallback);
return await rotateAppBackup(backupConfig, app, tag, options, progressCallback);
2021-07-14 11:07:19 -07:00
}
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();
2022-04-28 21:29:11 -07:00
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,
appConfig: null
};
const id = await backups.add(data);
2022-04-05 13:11:30 +02:00
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);
2022-04-05 13:11:30 +02:00
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}`);
2023-08-04 11:24:28 +05:30
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();
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
async function fullBackup(options, progressCallback) {
2021-07-14 11:07:19 -07:00
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
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` });
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
await locks.wait(`${locks.TYPE_APP_PREFIX}${app.id}`);
2021-09-16 13:59:03 -07:00
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`);
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
await locks.release(`${locks.TYPE_APP_PREFIX}${app.id}`);
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;
const mailBackupId = await backupMailWithTag(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);
const backupId = await backupBox(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
}