Files
cloudron-box/src/backuptask.js
T

563 lines
24 KiB
JavaScript
Raw Normal View History

2021-07-14 11:07:19 -07:00
'use strict';
exports = module.exports = {
fullBackup,
2025-07-18 10:56:52 +02:00
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'),
2025-08-14 11:17:38 +05:30
assert = require('node: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'),
crypto = require('node:crypto'),
2021-07-14 11:07:19 -07:00
DataLayout = require('./datalayout.js'),
database = require('./database.js'),
debug = require('debug')('box:backuptask'),
df = require('./df.js'),
2024-12-07 14:35:45 +01:00
locks = require('./locks.js'),
2025-08-14 11:17:38 +05:30
path = require('node:path'),
2021-07-14 11:07:19 -07:00
paths = require('./paths.js'),
2025-08-15 14:33:31 +05:30
{ Readable } = require('node:stream'),
2021-07-14 11:07:19 -07:00
safe = require('safetydance'),
services = require('./services.js'),
2025-08-15 14:33:31 +05:30
shell = require('./shell.js')('backuptask'),
stream = require('stream/promises');
2021-07-14 11:07:19 -07:00
const BACKUP_UPLOAD_CMD = path.join(__dirname, 'scripts/backupupload.js');
2025-08-01 22:58:19 +02:00
function addFileExtension(backupTarget, remotePath) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof remotePath, 'string');
const ext = backupFormat.api(backupTarget.format).getFileExtension(!!backupTarget.encyption);
return remotePath + ext;
}
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);
2024-11-06 14:53:41 +01:00
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
2025-08-01 14:54:32 +02:00
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}`);
2024-11-06 14:53:41 +01:00
// 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)}`);
}
2025-08-15 14:33:31 +05:30
async function uploadBackupInfo(backupTarget, remotePath, integrityMap) {
const sortedIntegrityMap = [...integrityMap.entries()].sort(([a], [b]) => a < b); // for readability, order the entries
const integrityDataJsonString = JSON.stringify(Object.fromEntries(sortedIntegrityMap), null, 2);
const integrityDataStream = Readable.from(integrityDataJsonString);
const integrityUploader = await backupTargets.storageApi(backupTarget).upload(backupTarget.config, `${remotePath}.backupinfo`);
await stream.pipeline(integrityDataStream, integrityUploader.stream);
await integrityUploader.finish();
return await crypto.sign(null /* algorithm */, integrityDataJsonString, backupTarget.integrityKeyPair.privateKey);
}
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) {
2022-04-04 14:13:27 -07:00
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-08-15 14:33:31 +05:30
const { stats, integrityMap } = await backupFormat.api(backupTarget.format).upload(backupTarget, remotePath, dataLayout, progressCallback);
progressCallback({ message: `Uploading integrity information to ${remotePath}.backupinfo` });
2025-08-15 14:33:31 +05:30
const signature = await uploadBackupInfo(backupTarget, remotePath, integrityMap);
return { stats, integrity: { signature } };
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(backupTarget, remotePath, progressCallback) {
assert.strictEqual(typeof backupTarget, '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, []);
await download(backupTarget, remotePath, 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');
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-08-02 19:09:21 +02:00
let { backupTarget, remotePath } = restoreConfig; // set when importing
if (!remotePath) {
const backup = await backups.get(restoreConfig.backupId);
if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'No such backup');
remotePath = backup.remotePath;
backupTarget = await backupTargets.get(backup.targetId);
}
2021-07-14 11:07:19 -07:00
2025-08-02 19:09:21 +02:00
await download(backupTarget, 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;
2022-04-04 14:13:27 -07:00
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}`;
}
2025-08-11 19:30:22 +05:30
let lastMessage = null; // the script communicates error result as a string
2022-04-28 21:29:11 -07:00
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)}`);
2025-08-11 19:30:22 +05:30
lastMessage = progress;
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
2025-08-11 19:30:22 +05:30
throw new BoxError(BoxError.EXTERNAL_ERROR, lastMessage.errorMessage);
2022-04-28 21:29:11 -07:00
}
2025-08-11 19:30:22 +05:30
return lastMessage.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
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension(backupTarget, `snapshot/box`);
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 = {
2025-08-01 22:58:19 +02:00
remotePath,
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
const { stats, integrity } = 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() });
2025-08-11 19:30:22 +05:30
return { stats, integrity };
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');
2022-04-04 14:13:27 -07:00
assert.strictEqual(typeof srcRemotePath, 'string');
assert.strictEqual(typeof destRemotePath, 'string');
assert.strictEqual(typeof progressCallback, 'function');
2022-04-30 16:01:42 -07:00
const startTime = new Date();
2025-08-02 01:46:29 +02:00
const [copyError] = await safe(backupTargets.storageApi(backupTarget).copy(backupTarget.config, srcRemotePath, destRemotePath, 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-08-11 19:30:22 +05:30
2025-08-13 21:29:34 +05:30
const [copyChecksumError] = await safe(backupTargets.storageApi(backupTarget).copy(backupTarget.config, `${srcRemotePath}.backupinfo`, `${destRemotePath}.backupinfo`, progressCallback));
2025-08-11 19:30:22 +05:30
if (copyChecksumError) {
debug(`copy: copied to ${destRemotePath} errored. error: ${copyChecksumError.message}`);
throw copyChecksumError;
}
2025-08-13 21:29:34 +05:30
debug(`copy: copied backupinfo successfully to ${destRemotePath}.backupinfo`);
}
async function backupBox(backupTarget, dependsOn, tag, options, progressCallback) {
2025-07-24 19:02:02 +02:00
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');
const { stats, integrity } = await uploadBoxSnapshot(backupTarget, progressCallback);
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension(backupTarget, `${tag}/box_v${constants.VERSION}`);
2021-07-14 11:07:19 -07:00
debug(`backupBox: rotating to id ${remotePath}`);
2021-07-14 11:07:19 -07:00
const data = {
2022-04-04 14:13:27 -07:00
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,
2024-12-10 20:52:29 +01:00
preserveSecs: options.preserveSecs || 0,
2025-07-25 07:44:25 +02:00
appConfig: null,
targetId: backupTarget.id,
stats,
integrity
2021-07-14 11:07:19 -07:00
};
const id = await backups.add(data);
2025-08-01 22:58:19 +02:00
const snapshotPath = addFileExtension(backupTarget, 'snapshot/box');
const [error] = await safe(copy(backupTarget, snapshotPath, 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;
2022-04-04 14:13:27 -07:00
return id;
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
}
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
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension(backupTarget, `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 = {
2022-04-04 14:13:27 -07:00
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
const { stats, integrity } = 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 });
2025-08-11 19:30:22 +05:30
return { stats, integrity };
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');
2025-07-18 10:56:52 +02:00
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
}
const { stats, integrity } = await uploadAppSnapshot(backupTarget, app, progressCallback);
const manifest = app.manifest;
const remotePath = addFileExtension(backupTarget, `${tag}/app_${app.fqdn}_v${manifest.version}`);
debug(`backupAppWithTag: rotating ${app.fqdn} to path ${remotePath}`);
const data = {
remotePath,
encryptionVersion: backupTarget.encryption ? 2 : null,
packageVersion: manifest.version,
type: backups.BACKUP_TYPE_APP,
state: backups.BACKUP_STATE_CREATING,
identifier: app.id,
dependsOn: [],
manifest,
preserveSecs: options.preserveSecs || 0,
appConfig: app,
targetId: backupTarget.id,
stats,
integrity
};
const id = await backups.add(data);
const snapshotPath = addFileExtension(backupTarget, `snapshot/app_${app.id}`);
const [error] = await safe(copy(backupTarget, snapshotPath, remotePath, progressCallback));
const state = error ? backups.BACKUP_STATE_ERROR : backups.BACKUP_STATE_NORMAL;
await backups.setState(id, state);
if (error) throw error;
2025-08-11 19:30:22 +05:30
return id;
2021-07-14 11:07:19 -07:00
}
async function backupApp(app, backupTarget, options, progressCallback) {
assert.strictEqual(typeof app, 'object');
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,'');
backupId = await backupAppWithTag(app, backupTarget, tag, options, progressCallback);
}
await locks.release(`${locks.TYPE_APP_BACKUP_PREFIX}${app.id}`);
return backupId;
}
2025-07-24 19:02:02 +02:00
async function uploadMailSnapshot(backupTarget, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof progressCallback, 'function');
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension(backupTarget, 'snapshot/mail');
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 = {
2025-08-01 22:58:19 +02:00
remotePath,
2025-07-24 19:02:02 +02:00
backupTarget,
dataLayout: new DataLayout(mailDataDir, []),
progressTag: 'mail'
};
progressCallback({ message: 'Uploading mail snapshot' });
const startTime = new Date();
const { stats, integrity } = await runBackupUpload(uploadConfig, progressCallback);
debug(`uploadMailSnapshot: took ${(new Date() - startTime)/1000} seconds`);
await backupTargets.setSnapshotInfo(backupTarget, 'mail', { timestamp: new Date().toISOString() });
2025-08-11 19:30:22 +05:30
return { stats, integrity };
}
async function backupMailWithTag(backupTarget, tag, options, progressCallback) {
2025-07-25 07:44:25 +02:00
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}`);
const { stats, integrity } = await uploadMailSnapshot(backupTarget, progressCallback);
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension(backupTarget, `${tag}/mail_v${constants.VERSION}`);
debug(`backupMailWithTag: rotating to ${remotePath}`);
const data = {
2022-04-04 14:13:27 -07:00
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,
2024-12-10 20:52:29 +01:00
preserveSecs: options.preserveSecs || 0,
2025-07-25 07:44:25 +02:00
appConfig: null,
targetId: backupTarget.id,
stats,
integrity
};
const id = await backups.add(data);
2025-08-01 22:58:19 +02:00
const snapshotPath = addFileExtension(backupTarget, 'snapshot/mail');
const [error] = await safe(copy(backupTarget, snapshotPath, 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;
2022-04-04 14:13:27 -07:00
return id;
}
async function downloadMail(backupTarget, remotePath, progressCallback) {
assert.strictEqual(typeof backupTarget, 'object');
assert.strictEqual(typeof remotePath, 'string');
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(backupTarget, remotePath, 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');
2022-04-04 14:13:27 -07:00
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`);
2021-11-02 17:59:08 -07:00
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` });
2025-07-18 10:56:52 +02:00
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`);
2025-07-18 10:56:52 +02:00
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
2022-04-04 14:13:27 -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 }));
2022-04-04 14:13:27 -07:00
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
}
2025-07-18 10:56:52 +02:00
// this function is called from external process
2025-07-24 19:02:02 +02:00
async function appBackup(appId, backupTargetId, options, progressCallback) {
2025-07-18 10:56:52 +02:00
assert.strictEqual(typeof appId, 'string');
2025-07-25 12:55:14 +02:00
assert.strictEqual(typeof backupTargetId, 'string');
2025-07-18 10:56:52 +02:00
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');
2025-07-18 10:56:52 +02:00
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);
2025-07-18 10:56:52 +02:00
await progressCallback({ percent: 100, message: `app ${app.fqdn} backup finished. Took ${(new Date() - startTime)/1000} seconds` });
return backupId;
}