diff --git a/CHANGES b/CHANGES index e26322272..a3a21c74c 100644 --- a/CHANGES +++ b/CHANGES @@ -1658,4 +1658,5 @@ * Fix issue where task logs were not getting rotated correctly * Add notification for box update * User enable/disable flag +* Check disk space before various operations like install, update, backup etc diff --git a/package-lock.json b/package-lock.json index 0656832ff..8eeb7f3ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3722,6 +3722,11 @@ "resolved": false, "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" }, + "pretty-bytes": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.3.0.tgz", + "integrity": "sha512-hjGrh+P926p4R4WbaB6OckyRtO0F0/lQBiT+0gnxjV+5kjPBrfVBFCsCLbMqVQeydvIoouYTCmmEURiH3R1Bdg==" + }, "process-nextick-args": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", diff --git a/package.json b/package.json index f5f898efa..b8ef2649b 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "passport-http-bearer": "^1.0.1", "passport-local": "^1.0.0", "passport-oauth2-client-password": "^0.1.2", + "pretty-bytes": "^5.3.0", "progress-stream": "^2.0.0", "proxy-middleware": "^0.15.0", "qrcode": "^1.3.3", diff --git a/src/backups.js b/src/backups.js index 096fd8aee..e64d988ba 100644 --- a/src/backups.js +++ b/src/backups.js @@ -52,6 +52,7 @@ var addons = require('./addons.js'), DatabaseError = require('./databaseerror.js'), DataLayout = require('./datalayout.js'), debug = require('debug')('box:backups'), + df = require('@sindresorhus/df'), eventlog = require('./eventlog.js'), fs = require('fs'), locker = require('./locker.js'), @@ -60,6 +61,7 @@ var addons = require('./addons.js'), path = require('path'), paths = require('./paths.js'), progressStream = require('progress-stream'), + prettyBytes = require('pretty-bytes'), safe = require('safetydance'), shell = require('./shell.js'), settings = require('./settings.js'), @@ -412,6 +414,34 @@ function saveFsMetadata(dataLayout, metadataFile, callback) { callback(); } +// the du call in the function below requires root +function checkFreeDiskSpace(backupConfig, dataLayout, callback) { + assert.strictEqual(typeof backupConfig, 'object'); + assert(dataLayout instanceof DataLayout, 'dataLayout must be a DataLayout'); + assert.strictEqual(typeof callback, 'function'); + + if (backupConfig.provider !== 'filesystem') return callback(); + + let used = 0; + for (let localPath of dataLayout.localPaths()) { + debug(`checkFreeDiskSpace: getting disk usage of ${localPath}`); + let result = safe.child_process.execSync(`du -Dsb ${localPath}`, { encoding: 'utf8' }); + if (!result) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, safe.error)); + used += parseInt(result, 10); + } + + debug(`checkFreeDiskSpace: ${used} bytes`); + + df.file(backupConfig.backupFolder).then(function (diskUsage) { + const needed = used + (1024 * 1024 * 1024); // check if there is atleast 1GB left afterwards + if (diskUsage.available <= needed) return callback(new BackupsError(BackupsError.EXTERNAL_ERROR, `Not enough disk space for backup. Needed: ${prettyBytes(needed)} Available: ${prettyBytes(diskUsage.available)}`)); + + callback(null); + }).catch(function (error) { + callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); + }); +} + // this function is called via backupupload (since it needs root to traverse app's directory) function upload(backupId, format, dataLayoutString, progressCallback, callback) { assert.strictEqual(typeof backupId, 'string'); @@ -427,29 +457,33 @@ function upload(backupId, format, dataLayoutString, progressCallback, callback) settings.getBackupConfig(function (error, backupConfig) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); - if (format === 'tgz') { - async.retry({ times: 5, interval: 20000 }, function (retryCallback) { - retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error + checkFreeDiskSpace(backupConfig, dataLayout, function (error) { + if (error) return callback(error); - tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) { - if (error) return retryCallback(error); + if (format === 'tgz') { + async.retry({ times: 5, interval: 20000 }, function (retryCallback) { + retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error - tarStream.on('progress', function(progress) { - const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024); - if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong - progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` }); + tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) { + if (error) return retryCallback(error); + + tarStream.on('progress', function(progress) { + const transferred = Math.round(progress.transferred/1024/1024), speed = Math.round(progress.speed/1024/1024); + if (!transferred && !speed) return progressCallback({ message: 'Uploading backup' }); // 0M@0Mbps looks wrong + progressCallback({ message: `Uploading backup ${transferred}M@${speed}Mbps` }); + }); + tarStream.on('error', retryCallback); // already returns BackupsError + + api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback); }); - tarStream.on('error', retryCallback); // already returns BackupsError - - api(backupConfig.provider).upload(backupConfig, getBackupFilePath(backupConfig, backupId, format), tarStream, retryCallback); - }); - }, callback); - } else { - async.series([ - saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`), - sync.bind(null, backupConfig, backupId, dataLayout, progressCallback) - ], callback); - } + }, callback); + } else { + async.series([ + saveFsMetadata.bind(null, dataLayout, `${dataLayout.localRoot()}/fsmetadata.json`), + sync.bind(null, backupConfig, backupId, dataLayout, progressCallback) + ], callback); + } + }); }); } @@ -693,7 +727,7 @@ function runBackupUpload(backupId, format, dataLayout, progressCallback, callbac callback(); }).on('message', function (message) { if (!message.result) return progressCallback(message); - debug(`runBackupUpload: result - ${message}`); + debug(`runBackupUpload: result - ${JSON.stringify(message)}`); result = message.result; }); }