diff --git a/src/apps.js b/src/apps.js index 2fa880bba..3d962d82d 100644 --- a/src/apps.js +++ b/src/apps.js @@ -16,12 +16,10 @@ exports = module.exports = { uninstall: uninstall, restore: restore, - restoreApp: restoreApp, update: update, backup: backup, - backupApp: backupApp, listBackups: listBackups, getLogs: getLogs, @@ -33,8 +31,6 @@ exports = module.exports = { checkManifestConstraints: checkManifestConstraints, - setRestorePoint: setRestorePoint, - autoupdateApps: autoupdateApps, // exported for testing @@ -48,7 +44,6 @@ var addons = require('./addons.js'), assert = require('assert'), async = require('async'), backups = require('./backups.js'), - BackupsError = require('./backups.js').BackupsError, certificates = require('./certificates.js'), config = require('./config.js'), constants = require('./constants.js'), @@ -62,7 +57,6 @@ var addons = require('./addons.js'), paths = require('./paths.js'), safe = require('safetydance'), semver = require('semver'), - shell = require('./shell.js'), spawn = require('child_process').spawn, split = require('split'), superagent = require('superagent'), @@ -70,26 +64,6 @@ var addons = require('./addons.js'), util = require('util'), validator = require('validator'); -var BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'), - RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'), - BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'); - -function debugApp(app, args) { - assert(!app || typeof app === 'object'); - - var prefix = app ? app.location : '(no app)'; - debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); -} - -function ignoreError(func) { - return function (callback) { - func(function (error) { - if (error) console.error('Ignored error:', error); - callback(); - }); - }; -} - // http://dustinsenos.com/articles/customErrorsInNode // http://code.google.com/p/v8/wiki/JavaScriptStackTraceApi function AppsError(reason, errorOrMessage) { @@ -787,20 +761,6 @@ function exec(appId, options, callback) { }); } -function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) { - assert.strictEqual(typeof appId, 'string'); - assert.strictEqual(typeof lastBackupId, 'string'); - assert.strictEqual(typeof lastBackupConfig, 'object'); - assert.strictEqual(typeof callback, 'function'); - - appdb.update(appId, { lastBackupId: lastBackupId, lastBackupConfig: lastBackupConfig }, function (error) { - if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such app')); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - return callback(null); - }); -} - function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { manifest } } assert.strictEqual(typeof updateInfo, 'object'); assert.strictEqual(typeof callback, 'function'); @@ -845,97 +805,6 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma }, callback); } -function canBackupApp(app) { - // only backup apps that are installed or pending configure or called from apptask. Rest of them are in some - // state not good for consistent backup (i.e addons may not have been setup completely) - return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) || - app.installationState === appdb.ISTATE_PENDING_CONFIGURE || - app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask - app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask -} - -// set the 'creation' date of lastBackup so that the backup persists across time based archival rules -// s3 does not allow changing creation time, so copying the last backup is easy way out for now -function reuseOldBackup(app, callback) { - assert.strictEqual(typeof app.lastBackupId, 'string'); - assert.strictEqual(typeof callback, 'function'); - - backups.copyLastBackup(app, function (error, newBackupId) { - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId); - - callback(null, newBackupId); - }); -} - -function createNewBackup(app, addonsToBackup, callback) { - assert.strictEqual(typeof app, 'object'); - assert(!addonsToBackup || typeof addonsToBackup, 'object'); - assert.strictEqual(typeof callback, 'function'); - - backups.getAppBackupUrl(app, function (error, result) { - if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - debugApp(app, 'backupApp: backup url:%s backup config url:%s', result.url, result.configUrl); - - async.series([ - ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), - addons.backupAddons.bind(null, app, addonsToBackup), - shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ]), - ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), - ], function (error) { - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - callback(null, result.id); - }); - }); -} - -function backupApp(app, addonsToBackup, callback) { - assert.strictEqual(typeof app, 'object'); - assert(!addonsToBackup || typeof addonsToBackup, 'object'); - assert.strictEqual(typeof callback, 'function'); - - var appConfig = null, backupFunction; - - if (!canBackupApp(app)) { - if (!app.lastBackupId) { - debugApp(app, 'backupApp: cannot backup app'); - return callback(new AppsError(AppsError.BAD_STATE, 'App not healthy and never backed up previously')); - } - - appConfig = app.lastBackupConfig; - backupFunction = reuseOldBackup.bind(null, app); - } else { - appConfig = { - manifest: app.manifest, - location: app.location, - portBindings: app.portBindings, - accessRestriction: app.accessRestriction, - memoryLimit: app.memoryLimit - }; - backupFunction = createNewBackup.bind(null, app, addonsToBackup); - - if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) { - return callback(safe.error); - } - } - - backupFunction(function (error, backupId) { - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - debugApp(app, 'backupApp: successful id:%s', backupId); - - setRestorePoint(app.id, backupId, appConfig, function (error) { - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - return callback(null, backupId); - }); - }); -} - function backup(appId, callback) { assert.strictEqual(typeof appId, 'string'); assert.strictEqual(typeof callback, 'function'); @@ -955,26 +824,6 @@ function backup(appId, callback) { }); } -function restoreApp(app, addonsToRestore, backupId, callback) { - assert.strictEqual(typeof app, 'object'); - assert.strictEqual(typeof addonsToRestore, 'object'); - assert.strictEqual(typeof backupId, 'string'); - assert.strictEqual(typeof callback, 'function'); - assert(app.lastBackupId); - - backups.getRestoreUrl(backupId, function (error, result) { - if (error && error.reason == BackupsError.EXTERNAL_ERROR) return callback(new AppsError(AppsError.EXTERNAL_ERROR, error.message)); - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - debugApp(app, 'restoreApp: restoreUrl:%s', result.url); - - shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) { - if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error)); - - addons.restoreAddons(app, addonsToRestore, callback); - }); - }); -} function listBackups(page, perPage, appId, callback) { assert(typeof page === 'number' && page > 0); diff --git a/src/apptask.js b/src/apptask.js index 860d2616b..8945d1ab4 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -35,6 +35,7 @@ var addons = require('./addons.js'), apps = require('./apps.js'), assert = require('assert'), async = require('async'), + backups = require('./backups.js'), certificates = require('./certificates.js'), clientdb = require('./clientdb.js'), config = require('./config.js'), @@ -436,7 +437,7 @@ function backup(app, callback) { async.series([ updateApp.bind(null, app, { installationProgress: '10, Backing up' }), - apps.backupApp.bind(null, app, app.manifest.addons), + backups.backupApp.bind(null, app, app.manifest.addons), // done! function (callback) { @@ -501,7 +502,7 @@ function restore(app, callback) { createVolume.bind(null, app), updateApp.bind(null, app, { installationProgress: '70, Download backup and restore addons' }), - apps.restoreApp.bind(null, app, app.manifest.addons, backupId), + backups.restoreApp.bind(null, app, app.manifest.addons, backupId), updateApp.bind(null, app, { installationProgress: '75, Creating container' }), createContainer.bind(null, app), diff --git a/src/backups.js b/src/backups.js index 813f6854f..a6a79539f 100644 --- a/src/backups.js +++ b/src/backups.js @@ -12,17 +12,61 @@ exports = module.exports = { copyLastBackup: copyLastBackup, - getBackupCredentials: getBackupCredentials + ensureBackup: ensureBackup, + + backup: backup, + backupApp: backupApp, + restoreApp: restoreApp, + + getBackupCredentials: getBackupCredentials, + + backupBoxAndApps: backupBoxAndApps }; -var assert = require('assert'), +var addons = require('./addons.js'), + appdb = require('./appdb.js'), + apps = require('./apps.js'), + async = require('async'), + assert = require('assert'), backupdb = require('./backupdb.js'), caas = require('./storage/caas.js'), config = require('./config.js'), + DatabaseError = require('./databaseerror.js'), debug = require('debug')('box:backups'), + locker = require('./locker.js'), + path = require('path'), + paths = require('./paths.js'), + progress = require('./progress.js'), s3 = require('./storage/s3.js'), + safe = require('safetydance'), + shell = require('./shell.js'), settings = require('./settings.js'), - util = require('util'); + util = require('util'), + webhooks = require('./webhooks.js'); + +var BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'), + BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'), + BACKUP_APP_CMD = path.join(__dirname, 'scripts/backupapp.sh'), + RESTORE_APP_CMD = path.join(__dirname, 'scripts/restoreapp.sh'), + BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'); + +var NOOP_CALLBACK = function (error) { if (error) debug(error); }; + +function debugApp(app, args) { + assert(!app || typeof app === 'object'); + + var prefix = app ? app.location : '(no app)'; + debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); +} + +function ignoreError(func) { + return function (callback) { + func(function (error) { + if (error) console.error('Ignored error:', error); + callback(); + }); + }; +} function BackupsError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); @@ -45,6 +89,7 @@ function BackupsError(reason, errorOrMessage) { util.inherits(BackupsError, Error); BackupsError.EXTERNAL_ERROR = 'external error'; BackupsError.INTERNAL_ERROR = 'internal error'; +BackupsError.BAD_STATE = 'bad state'; BackupsError.MISSING_CREDENTIALS = 'missing credentials'; // choose which storage backend we use for test purpose we use s3 @@ -203,3 +248,246 @@ function copyLastBackup(app, callback) { }); }); } + +function backupBoxWithAppBackupIds(appBackupIds, callback) { + assert(util.isArray(appBackupIds)); + + getBackupUrl(appBackupIds, function (error, result) { + if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, error.message)); + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + debug('backupBoxWithAppBackupIds: %j', result); + + async.series([ + ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), + shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.s3Url, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ]), + ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), + ], function (error) { + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + debug('backupBoxWithAppBackupIds: success'); + + webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) { + if (error) return callback(error); + callback(null, result.id); + }); + }); + }); +} + +// this function expects you to have a lock +// function backupBox(callback) { +// apps.getAll(function (error, allApps) { +// if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); +// +// var appBackupIds = allApps.map(function (app) { return app.lastBackupId; }); +// appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up +// +// backupBoxWithAppBackupIds(appBackupIds, callback); +// }); +// } + +// this function expects you to have a lock +function backupBoxAndApps(callback) { + callback = callback || NOOP_CALLBACK; + + apps.getAll(function (error, allApps) { + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + var processed = 0; + var step = 100/(allApps.length+1); + + progress.set(progress.BACKUP, processed, ''); + + async.mapSeries(allApps, function iterator(app, iteratorCallback) { + ++processed; + + backupApp(app, app.manifest.addons, function (error, backupId) { + if (error && error.reason !== BackupsError.BAD_STATE) { + debugApp(app, 'Unable to backup', error); + return iteratorCallback(error); + } + + progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location); + + iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up + }); + }, function appsBackedUp(error, backupIds) { + if (error) { + progress.set(progress.BACKUP, 100, error.message); + return callback(error); + } + + backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up + + backupBoxWithAppBackupIds(backupIds, function (error, filename) { + progress.set(progress.BACKUP, 100, error ? error.message : ''); + + callback(error, filename); + }); + }); + }); +} + +function backup(callback) { + assert.strictEqual(typeof callback, 'function'); + + var error = locker.lock(locker.OP_FULL_BACKUP); + if (error) return callback(new BackupsError(BackupsError.BAD_STATE, error.message)); + + // ensure tools can 'wait' on progress + progress.set(progress.BACKUP, 0, 'Starting'); + + // start the backup operation in the background + backupBoxAndApps(function (error) { + if (error) console.error('backup failed.', error); + + locker.unlock(locker.OP_FULL_BACKUP); + }); + + callback(null); +} + +function ensureBackup(callback) { + callback = callback || NOOP_CALLBACK; + + getPaged(1, 1, function (error, backups) { + if (error) { + debug('Unable to list backups', error); + return callback(error); // no point trying to backup if appstore is down + } + + if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago + debug('Previous backup was %j, no need to backup now', backups[0]); + return callback(null); + } + + backup(callback); + }); +} + + +function canBackupApp(app) { + // only backup apps that are installed or pending configure or called from apptask. Rest of them are in some + // state not good for consistent backup (i.e addons may not have been setup completely) + return (app.installationState === appdb.ISTATE_INSTALLED && app.health === appdb.HEALTH_HEALTHY) || + app.installationState === appdb.ISTATE_PENDING_CONFIGURE || + app.installationState === appdb.ISTATE_PENDING_BACKUP || // called from apptask + app.installationState === appdb.ISTATE_PENDING_UPDATE; // called from apptask +} + +// set the 'creation' date of lastBackup so that the backup persists across time based archival rules +// s3 does not allow changing creation time, so copying the last backup is easy way out for now +function reuseOldBackup(app, callback) { + assert.strictEqual(typeof app.lastBackupId, 'string'); + assert.strictEqual(typeof callback, 'function'); + + copyLastBackup(app, function (error, newBackupId) { + if (error) return callback(error); + + debugApp(app, 'reuseOldBackup: reused old backup %s as %s', app.lastBackupId, newBackupId); + + callback(null, newBackupId); + }); +} + +function createNewBackup(app, addonsToBackup, callback) { + assert.strictEqual(typeof app, 'object'); + assert(!addonsToBackup || typeof addonsToBackup, 'object'); + assert.strictEqual(typeof callback, 'function'); + + getAppBackupUrl(app, function (error, result) { + if (error) return callback(error); + + debugApp(app, 'backupApp: backup url:%s backup config url:%s', result.url, result.configUrl); + + async.series([ + ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), + addons.backupAddons.bind(null, app, addonsToBackup), + shell.sudo.bind(null, 'backupApp', [ BACKUP_APP_CMD, result.s3ConfigUrl, result.s3DataUrl, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ]), + ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), + ], function (error) { + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + callback(null, result.id); + }); + }); +} + +function setRestorePoint(appId, lastBackupId, lastBackupConfig, callback) { + assert.strictEqual(typeof appId, 'string'); + assert.strictEqual(typeof lastBackupId, 'string'); + assert.strictEqual(typeof lastBackupConfig, 'object'); + assert.strictEqual(typeof callback, 'function'); + + appdb.update(appId, { lastBackupId: lastBackupId, lastBackupConfig: lastBackupConfig }, function (error) { + if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new BackupsError(BackupsError.NOT_FOUND, 'No such app')); + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + return callback(null); + }); +} + +function backupApp(app, addonsToBackup, callback) { + assert.strictEqual(typeof app, 'object'); + assert(!addonsToBackup || typeof addonsToBackup, 'object'); + assert.strictEqual(typeof callback, 'function'); + + var appConfig = null, backupFunction; + + if (!canBackupApp(app)) { + if (!app.lastBackupId) { + debugApp(app, 'backupApp: cannot backup app'); + return callback(new BackupsError(BackupsError.BAD_STATE, 'App not healthy and never backed up previously')); + } + + appConfig = app.lastBackupConfig; + backupFunction = reuseOldBackup.bind(null, app); + } else { + appConfig = { + manifest: app.manifest, + location: app.location, + portBindings: app.portBindings, + accessRestriction: app.accessRestriction, + memoryLimit: app.memoryLimit + }; + backupFunction = createNewBackup.bind(null, app, addonsToBackup); + + if (!safe.fs.writeFileSync(path.join(paths.DATA_DIR, app.id + '/config.json'), JSON.stringify(appConfig), 'utf8')) { + return callback(safe.error); + } + } + + backupFunction(function (error, backupId) { + if (error) return callback(error); + + debugApp(app, 'backupApp: successful id:%s', backupId); + + setRestorePoint(app.id, backupId, appConfig, function (error) { + if (error) return callback(error); + + return callback(null, backupId); + }); + }); +} + + +function restoreApp(app, addonsToRestore, backupId, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof addonsToRestore, 'object'); + assert.strictEqual(typeof backupId, 'string'); + assert.strictEqual(typeof callback, 'function'); + assert(app.lastBackupId); + + getRestoreUrl(backupId, function (error, result) { + if (error) return callback(error); + + debugApp(app, 'restoreApp: restoreUrl:%s', result.url); + + shell.sudo('restoreApp', [ RESTORE_APP_CMD, app.id, result.url, result.backupKey, result.sessionToken ], function (error) { + if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + + addons.restoreAddons(app, addonsToRestore, callback); + }); + }); +} diff --git a/src/cloudron.js b/src/cloudron.js index 6a7e021f5..edef30c1c 100644 --- a/src/cloudron.js +++ b/src/cloudron.js @@ -14,9 +14,7 @@ exports = module.exports = { updateToLatest: updateToLatest, update: update, reboot: reboot, - backup: backup, retire: retire, - ensureBackup: ensureBackup, isConfiguredSync: isConfiguredSync, @@ -30,11 +28,9 @@ exports = module.exports = { }; var apps = require('./apps.js'), - AppsError = require('./apps.js').AppsError, assert = require('assert'), async = require('async'), backups = require('./backups.js'), - BackupsError = require('./backups.js').BackupsError, clientdb = require('./clientdb.js'), config = require('./config.js'), debug = require('debug')('box:cloudron'), @@ -58,12 +54,9 @@ var apps = require('./apps.js'), UserError = user.UserError, userdb = require('./userdb.js'), util = require('util'), - uuid = require('node-uuid'), - webhooks = require('./webhooks.js'); + uuid = require('node-uuid'); var REBOOT_CMD = path.join(__dirname, 'scripts/reboot.sh'), - BACKUP_BOX_CMD = path.join(__dirname, 'scripts/backupbox.sh'), - BACKUP_SWAP_CMD = path.join(__dirname, 'scripts/backupswap.sh'), INSTALLER_UPDATE_URL = 'http://127.0.0.1:2020/api/v1/installer/update', RETIRE_CMD = path.join(__dirname, 'scripts/retire.sh'); @@ -74,22 +67,6 @@ var gUpdatingDns = false, // flag for dns update reentrancy gAppstoreUserDetails = {}, gIsConfigured = null; // cached configured state so that return value is synchronous. null means we are not initialized yet -function debugApp(app, args) { - assert(!app || typeof app === 'object'); - - var prefix = app ? app.location : '(no app)'; - debug(prefix + ' ' + util.format.apply(util, Array.prototype.slice.call(arguments, 1))); -} - -function ignoreError(func) { - return function (callback) { - func(function (error) { - if (error) console.error('Ignored error:', error); - callback(); - }); - }; -} - function CloudronError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); assert(errorOrMessage instanceof Error || typeof errorOrMessage === 'string' || typeof errorOrMessage === 'undefined'); @@ -562,7 +539,7 @@ function doUpgrade(boxUpdateInfo, callback) { progress.set(progress.UPDATE, 5, 'Backing up for upgrade'); - backupBoxAndApps(function (error) { + backups.backupBoxAndApps(function (error) { if (error) return upgradeError(error); superagent.post(config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/upgrade') @@ -591,7 +568,7 @@ function doUpdate(boxUpdateInfo, callback) { progress.set(progress.UPDATE, 5, 'Backing up for update'); - backupBoxAndApps(function (error) { + backups.backupBoxAndApps(function (error) { if (error) return updateError(error); // NOTE: the args here are tied to the installer revision, box code and appstore provisioning logic @@ -638,123 +615,6 @@ function doUpdate(boxUpdateInfo, callback) { }); } -function backup(callback) { - assert.strictEqual(typeof callback, 'function'); - - var error = locker.lock(locker.OP_FULL_BACKUP); - if (error) return callback(new CloudronError(CloudronError.BAD_STATE, error.message)); - - // ensure tools can 'wait' on progress - progress.set(progress.BACKUP, 0, 'Starting'); - - // start the backup operation in the background - backupBoxAndApps(function (error) { - if (error) console.error('backup failed.', error); - - locker.unlock(locker.OP_FULL_BACKUP); - }); - - callback(null); -} - -function ensureBackup(callback) { - callback = callback || NOOP_CALLBACK; - - backups.getPaged(1, 1, function (error, backups) { - if (error) { - debug('Unable to list backups', error); - return callback(error); // no point trying to backup if appstore is down - } - - if (backups.length !== 0 && (new Date() - new Date(backups[0].creationTime) < 23 * 60 * 60 * 1000)) { // ~1 day ago - debug('Previous backup was %j, no need to backup now', backups[0]); - return callback(null); - } - - backup(callback); - }); -} - -function backupBoxWithAppBackupIds(appBackupIds, callback) { - assert(util.isArray(appBackupIds)); - - backups.getBackupUrl(appBackupIds, function (error, result) { - if (error && error.reason === BackupsError.EXTERNAL_ERROR) return callback(new CloudronError(CloudronError.EXTERNAL_ERROR, error.message)); - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - - debug('backupBoxWithAppBackupIds: %j', result); - - async.series([ - ignoreError(shell.sudo.bind(null, 'mountSwap', [ BACKUP_SWAP_CMD, '--on' ])), - shell.sudo.bind(null, 'backupBox', [ BACKUP_BOX_CMD, result.s3Url, result.accessKeyId, result.secretAccessKey, result.sessionToken, result.region, result.backupKey ]), - ignoreError(shell.sudo.bind(null, 'unmountSwap', [ BACKUP_SWAP_CMD, '--off' ])), - ], function (error) { - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - - debug('backupBoxWithAppBackupIds: success'); - - webhooks.backupDone(result.id, null /* app */, appBackupIds, function (error) { - if (error) return callback(error); - callback(null, result.id); - }); - }); - }); -} - -// this function expects you to have a lock -function backupBox(callback) { - apps.getAll(function (error, allApps) { - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - - var appBackupIds = allApps.map(function (app) { return app.lastBackupId; }); - appBackupIds = appBackupIds.filter(function (id) { return id !== null; }); // remove apps that were never backed up - - backupBoxWithAppBackupIds(appBackupIds, callback); - }); -} - -// this function expects you to have a lock -function backupBoxAndApps(callback) { - callback = callback || NOOP_CALLBACK; - - apps.getAll(function (error, allApps) { - if (error) return callback(new CloudronError(CloudronError.INTERNAL_ERROR, error)); - - var processed = 0; - var step = 100/(allApps.length+1); - - progress.set(progress.BACKUP, processed, ''); - - async.mapSeries(allApps, function iterator(app, iteratorCallback) { - ++processed; - - apps.backupApp(app, app.manifest.addons, function (error, backupId) { - if (error && error.reason !== AppsError.BAD_STATE) { - debugApp(app, 'Unable to backup', error); - return iteratorCallback(error); - } - - progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location); - - iteratorCallback(null, backupId || null); // clear backupId if is in BAD_STATE and never backed up - }); - }, function appsBackedUp(error, backupIds) { - if (error) { - progress.set(progress.BACKUP, 100, error.message); - return callback(error); - } - - backupIds = backupIds.filter(function (id) { return id !== null; }); // remove apps in bad state that were never backed up - - backupBoxWithAppBackupIds(backupIds, function (error, filename) { - progress.set(progress.BACKUP, 100, error ? error.message : ''); - - callback(error, filename); - }); - }); - }); -} - function installAppBundle(callback) { callback = callback || NOOP_CALLBACK; diff --git a/src/cron.js b/src/cron.js index c0c3b0685..3872b8590 100644 --- a/src/cron.js +++ b/src/cron.js @@ -7,6 +7,7 @@ exports = module.exports = { var apps = require('./apps.js'), assert = require('assert'), + backups = require('./backups.js'), certificates = require('./certificates.js'), cloudron = require('./cloudron.js'), config = require('./config.js'), @@ -65,7 +66,7 @@ function recreateJobs(unusedTimeZone, callback) { if (gBackupJob) gBackupJob.stop(); gBackupJob = new CronJob({ cronTime: '00 00 */4 * * *', // every 4 hours - onTick: cloudron.ensureBackup, + onTick: backups.ensureBackup, start: true, timeZone: allSettings[settings.TIME_ZONE_KEY] }); diff --git a/src/routes/backups.js b/src/routes/backups.js index cc4d13ad5..10f38dd55 100644 --- a/src/routes/backups.js +++ b/src/routes/backups.js @@ -1,5 +1,3 @@ -/* jslint node:true */ - 'use strict'; exports = module.exports = { @@ -11,8 +9,6 @@ exports = module.exports = { var assert = require('assert'), backups = require('../backups.js'), BackupsError = require('../backups.js').BackupsError, - cloudron = require('../cloudron.js'), - CloudronError = require('../cloudron.js').CloudronError, HttpError = require('connect-lastmile').HttpError, HttpSuccess = require('connect-lastmile').HttpSuccess; @@ -34,8 +30,8 @@ function get(req, res, next) { function create(req, res, next) { // note that cloudron.backup only waits for backup initiation and not for backup to complete // backup progress can be checked up ny polling the progress api call - cloudron.backup(function (error) { - if (error && error.reason === CloudronError.BAD_STATE) return next(new HttpError(409, error.message)); + backups.backup(function (error) { + if (error && error.reason === BackupsError.BAD_STATE) return next(new HttpError(409, error.message)); if (error) return next(new HttpError(500, error)); next(new HttpSuccess(202, {}));