backups: fix app restore with tgz

This commit is contained in:
Girish Ramakrishnan
2025-07-25 12:55:14 +02:00
parent 83ab701d02
commit fc4da4408c
5 changed files with 43 additions and 32 deletions

View File

@@ -1260,12 +1260,12 @@ async function scheduleTask(appId, installationState, taskId, auditSource) {
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof auditSource, 'object');
const backupConfig = await backupTargets.getConfig();
const backupTarget = await backupTargets.getPrimary();
let memoryLimit = 400;
if (installationState === exports.ISTATE_PENDING_CLONE || installationState === exports.ISTATE_PENDING_RESTORE
|| installationState === exports.ISTATE_PENDING_IMPORT || installationState === exports.ISTATE_PENDING_UPDATE) {
memoryLimit = backupConfig.limits?.memoryLimit ? Math.max(backupConfig.limits.memoryLimit/1024/1024, 400) : 400;
memoryLimit = backupTarget.limits?.memoryLimit ? Math.max(backupTarget.limits.memoryLimit/1024/1024, 400) : 400;
} else if (installationState === exports.ISTATE_PENDING_DATA_DIR_MIGRATION) {
memoryLimit = 1024; // cp takes more memory than we think
}
@@ -2302,12 +2302,12 @@ async function restore(app, backupId, auditSource) {
if (error) throw error;
// for empty or null backupId, use existing manifest to mimic a reinstall
const backupInfo = backupId ? await backups.get(backupId) : { manifest: app.manifest };
if (!backupInfo) throw new BoxError(BoxError.BAD_FIELD, 'No such backup');
const manifest = backupInfo.manifest;
const backup = backupId ? await backups.get(backupId) : { manifest: app.manifest };
if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'No such backup');
const manifest = backup.manifest;
if (!manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not get restore manifest');
if (backupInfo.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool');
if (backup.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and has to be restored using the CLI tool');
// re-validate because this new box version may not accept old configs
error = await checkManifest(manifest);
@@ -2326,7 +2326,7 @@ async function restore(app, backupId, auditSource) {
values.inboxName = values.inboxDomain = null;
}
const restoreConfig = { remotePath: backupInfo.remotePath, backupFormat: backupInfo.format };
const restoreConfig = { backupId: backup.id };
const task = {
args: {
@@ -2340,7 +2340,7 @@ async function restore(app, backupId, auditSource) {
const taskId = await addTask(appId, exports.ISTATE_PENDING_RESTORE, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app, backupId: backupInfo.id, remotePath: backupInfo.remotePath, fromManifest: app.manifest, toManifest: manifest, taskId });
await eventlog.add(eventlog.ACTION_APP_RESTORE, auditSource, { app, backupId: backup.id, remotePath: backup.remotePath, fromManifest: app.manifest, toManifest: manifest, taskId });
return { taskId };
}
@@ -2405,7 +2405,7 @@ async function exportApp(app, data, auditSource) {
if (!canBackupApp(app)) throw new BoxError(BoxError.BAD_STATE, 'App cannot be backed up in this state');
const backupTarget = await backupTargets._getDefault();
const backupTarget = await backupTargets.getPrimary();
const taskId = await tasks.add(`${tasks.TASK_APP_BACKUP_PREFIX}${app.id}`, [ appId, backupTarget.id, { snapshotOnly: true } ]);
safe(tasks.startTask(taskId, {}), { debug }); // background
return { taskId };
@@ -2427,13 +2427,13 @@ async function clone(app, data, user, auditSource) {
assert.strictEqual(typeof subdomain, 'string');
assert.strictEqual(typeof domain, 'string');
const backupInfo = await backups.get(backupId);
const backup = await backups.get(backupId);
if (!backupInfo) throw new BoxError(BoxError.NOT_FOUND, 'Backup not found');
if (!backupInfo.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not detect restore manifest');
if (backupInfo.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned');
if (!backup) throw new BoxError(BoxError.NOT_FOUND, 'Backup not found');
if (!backup.manifest) throw new BoxError(BoxError.EXTERNAL_ERROR, 'Could not detect restore manifest');
if (backup.encryptionVersion === 1) throw new BoxError(BoxError.BAD_FIELD, 'This encrypted backup was created with an older Cloudron version and cannot be cloned');
const manifest = backupInfo.manifest, appStoreId = app.appStoreId;
const manifest = backup.manifest, appStoreId = app.appStoreId;
let error = validateSecondaryDomains(data.secondaryDomains || {}, manifest);
if (error) throw error;
@@ -2460,7 +2460,7 @@ async function clone(app, data, user, auditSource) {
const newAppId = uuid.v4();
// label, checklist intentionally omitted . icon is loaded in apptask from the backup
const dolly = _.pick(backupInfo.appConfig || app, ['memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices',
const dolly = _.pick(backup.appConfig || app, ['memoryLimit', 'cpuQuota', 'crontab', 'reverseProxyConfig', 'env', 'servicesConfig', 'tags', 'devices',
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain', 'debugMode',
'enableTurn', 'enableRedis', 'mounts', 'enableBackup', 'enableAutomaticUpdate', 'accessRestriction', 'operators', 'sso',
'notes', 'checklist']);
@@ -2482,7 +2482,7 @@ async function clone(app, data, user, auditSource) {
if (addError && addError.reason === BoxError.ALREADY_EXISTS) throw getDuplicateErrorDetails(addError.message, locations, portBindings);
if (addError) throw addError;
const restoreConfig = { remotePath: backupInfo.remotePath, backupFormat: backupInfo.format };
const restoreConfig = { backupId: backup.id };
const task = {
args: { restoreConfig, overwriteDns, skipDnsSetup, oldManifest: null },
values: {},
@@ -2496,7 +2496,7 @@ async function clone(app, data, user, auditSource) {
newApp.redirectDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
newApp.aliasDomains.forEach(function (ad) { ad.fqdn = dns.fqdn(ad.subdomain, ad.domain); });
await eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: app.id, backupId, remotePath: backupInfo.remotePath, oldApp: app, newApp, taskId });
await eventlog.add(eventlog.ACTION_APP_CLONE, auditSource, { appId: newAppId, oldAppId: app.id, backupId, remotePath: backup.remotePath, oldApp: app, newApp, taskId });
return { id: newAppId, taskId };
}
@@ -2507,7 +2507,7 @@ async function unarchive(archive, data, auditSource) {
assert(auditSource && typeof auditSource === 'object');
const backup = await backups.get(archive.backupId);
const restoreConfig = { remotePath: backup.remotePath, backupFormat: backup.format };
const restoreConfig = { backupId: backup.id };
const subdomain = data.subdomain.toLowerCase(),
domain = data.domain.toLowerCase(),
@@ -2780,7 +2780,7 @@ async function backup(app, auditSource) {
if (!canBackupApp(app)) throw new BoxError(BoxError.BAD_STATE, 'App cannot be backed up in this state');
const backupTarget = await backupTargets._getDefault();
const backupTarget = await backupTargets.getPrimary();
const taskId = await tasks.add(`${tasks.TASK_APP_BACKUP_PREFIX}${app.id}`, [ app.id, backupTarget.id, { snapshotOnly: false } ]);
@@ -2833,11 +2833,12 @@ async function getBackupDownloadStream(app, backupId) {
if (backup.identifier !== app.id) throw new BoxError(BoxError.NOT_FOUND, 'Backup not found'); // some other app's backup
if (backup.format !== 'tgz') throw new BoxError(BoxError.BAD_STATE, 'only tgz backups can be downloaded');
const backupConfig = await backupTargets.getConfig();
const backupTarget = await backupTargets.get(backup.targetId);
if (!backupTarget) throw new BoxError(BoxError.NOT_FOUND, 'Backup target not found'); // not possible
const ps = new PassThrough();
const stream = await storage.api(backupConfig.provider).download(backupConfig, tgz.getBackupFilePath(backupConfig, backup.remotePath));
const stream = await storage.api(backupTarget.provider).download(backupTarget.config, tgz.getBackupFilePath(backupTarget, backup.remotePath));
stream.on('error', function(error) {
debug(`getBackupDownloadStream: read stream error: ${error.message}`);
ps.emit('error', new BoxError(BoxError.EXTERNAL_ERROR, error));