diff --git a/src/apps.js b/src/apps.js index fdd30cfda..1651bb937 100644 --- a/src/apps.js +++ b/src/apps.js @@ -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)); diff --git a/src/backuptargets.js b/src/backuptargets.js index ec39fe26a..d08a80012 100644 --- a/src/backuptargets.js +++ b/src/backuptargets.js @@ -2,6 +2,7 @@ exports = module.exports = { get, + getPrimary, list, add, del, @@ -167,6 +168,12 @@ async function get(id) { return postProcess(results[0]); } +async function getPrimary() { + const results = await database.query(`SELECT ${BACKUP_TARGET_FIELDS} FROM backupTargets WHERE main=?`, [ true ]); + if (results.length === 0) return null; + return postProcess(results[0]); +} + async function update(target, data) { assert.strictEqual(typeof target, 'object'); assert(data && typeof data === 'object'); diff --git a/src/backuptask.js b/src/backuptask.js index ed0bf9057..5dd52f858 100644 --- a/src/backuptask.js +++ b/src/backuptask.js @@ -81,16 +81,15 @@ async function upload(remotePath, targetId, dataLayoutString, progressCallback) await backupFormat.api(backupTarget.format).upload(backupTarget, remotePath, dataLayout, progressCallback); } -async function download(backupConfig, remotePath, format, dataLayout, progressCallback) { - assert.strictEqual(typeof backupConfig, 'object'); +async function download(backupTarget, remotePath, dataLayout, progressCallback) { + assert.strictEqual(typeof backupTarget, 'object'); assert.strictEqual(typeof remotePath, 'string'); - assert.strictEqual(typeof format, 'string'); assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout'); assert.strictEqual(typeof progressCallback, 'function'); - debug(`download: Downloading ${remotePath} of format ${format} (encrypted: ${!!backupConfig.encryption}) to ${dataLayout.toString()}`); + debug(`download: Downloading ${remotePath} of format ${backupTarget.format} (encrypted: ${!!backupTarget.encryption}) to ${dataLayout.toString()}`); - await backupFormat.api(format).download(backupConfig, remotePath, dataLayout, progressCallback); + await backupFormat.api(backupTarget.format).download(backupTarget, remotePath, dataLayout, progressCallback); } async function restore(backupConfig, remotePath, progressCallback) { @@ -122,9 +121,10 @@ async function downloadApp(app, restoreConfig, progressCallback) { const dataLayout = new DataLayout(appDataDir, app.storageVolumeId ? [{ localDir: await apps.getStorageDir(app), remoteDir: 'data' }] : []); const startTime = new Date(); - const backupConfig = restoreConfig.backupConfig || await backupTargets.getConfig(); + const backup = await backups.get(restoreConfig.backupId); + const backupTarget = await backupTargets.get(backup.targetId); - await download(backupConfig, restoreConfig.remotePath, restoreConfig.backupFormat, dataLayout, progressCallback); + await download(backupTarget, backup.remotePath, dataLayout, progressCallback); debug('downloadApp: time: %s', (new Date() - startTime)/1000); } @@ -518,6 +518,7 @@ async function fullBackup(backupTargetId, options, progressCallback) { // this function is called from external process async function appBackup(appId, backupTargetId, options, progressCallback) { assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof backupTargetId, 'string'); assert.strictEqual(typeof options, 'object'); assert.strictEqual(typeof progressCallback, 'function'); diff --git a/src/taskworker.js b/src/taskworker.js index f4157e919..3bc64c93d 100755 --- a/src/taskworker.js +++ b/src/taskworker.js @@ -70,6 +70,7 @@ async function setupNetworking() { // 50 - internal error , 70 - SIGTERM exit function exitSync(status) { if (status.error) fs.write(logFd, status.error.stack + '\n', function () {}); + fs.write(logFd, `Exiting with code ${status.code}\n`, function () {}); fs.fsyncSync(logFd); fs.closeSync(logFd); process.exit(status.code); @@ -127,10 +128,11 @@ async function main() { percent: 100 }; + await safe(tasks.setCompleted(taskId, progress), { debug }); + debug(`Task took ${(new Date() - startTime)/1000} seconds`); - await safe(tasks.setCompleted(taskId, progress), { debug }); - exitSync({ error: runError, code: runError instanceof BoxError ? 0 : 50 }); // handled error vs run time crash + exitSync({ error: runError, code: (!runError || runError instanceof BoxError) ? 0 : 50 }); // handled error vs run time crash } main(); diff --git a/src/updater.js b/src/updater.js index 0bdf125cf..56f07de86 100644 --- a/src/updater.js +++ b/src/updater.js @@ -223,8 +223,8 @@ async function startBoxUpdateTask(options, auditSource) { const [error] = await safe(locks.acquire(locks.TYPE_BOX_UPDATE_TASK)); if (error) throw new BoxError(BoxError.BAD_STATE, `Another update task is in progress: ${error.message}`); - const backupConfig = await backupTargets.getConfig(); - const memoryLimit = backupConfig.limits?.memoryLimit ? Math.max(backupConfig.limits.memoryLimit/1024/1024, 400) : 400; + const backupTarget = await backupTargets.getPrimary(); + const memoryLimit = backupTarget.limits?.memoryLimit ? Math.max(backupTarget.limits.memoryLimit/1024/1024, 400) : 400; const taskId = await tasks.add(tasks.TASK_BOX_UPDATE, [ boxUpdateInfo, options ]); await eventlog.add(eventlog.ACTION_UPDATE, auditSource, { taskId, boxUpdateInfo });