diff --git a/migrations/20210517194116-backups-provider-mountpoint.js b/migrations/20210517194116-backups-provider-mountpoint.js new file mode 100644 index 000000000..4677be914 --- /dev/null +++ b/migrations/20210517194116-backups-provider-mountpoint.js @@ -0,0 +1,27 @@ +'use strict'; + +exports.up = function(db, callback) { + db.all('SELECT value FROM settings WHERE name="backup_config"', function (error, results) { + if (error || results.length === 0) return callback(error); + + const backupConfig = JSON.parse(results[0].value); + if (backupConfig.provider === 'sshfs' || backupConfig.provider === 'cifs' || backupConfig.provider === 'nfs' || backupConfig.externalDisk) { + backupConfig.chown = backupConfig.provider === 'nfs' || backupConfig.externalDisk; // sshfs and cifs handle ownership through the mount args + backupConfig.preserveAttributes = !!backupConfig.externalDisk; + backupConfig.provider = 'mountpoint'; + if (backupConfig.externalDisk) { + backupConfig.mountPoint = backupConfig.backupFolder; + backupConfig.prefix = ''; + delete backupConfig.backupFolder; + delete backupConfig.externalDisk; + } + db.runSql('UPDATE settings SET value=? WHERE name="backup_config"', [JSON.stringify(backupConfig)], callback); + } else { + callback(); + } + }); +}; + +exports.down = function(db, callback) { + callback(); +}; diff --git a/src/backups.js b/src/backups.js index f2badcc85..053ba83af 100644 --- a/src/backups.js +++ b/src/backups.js @@ -96,6 +96,7 @@ function api(provider) { case 'nfs': return require('./storage/filesystem.js'); case 'cifs': return require('./storage/filesystem.js'); case 'sshfs': return require('./storage/filesystem.js'); + case 'mountpoint': return require('./storage/filesystem.js'); case 's3': return require('./storage/s3.js'); case 'gcs': return require('./storage/gcs.js'); case 'filesystem': return require('./storage/filesystem.js'); @@ -1605,7 +1606,7 @@ function checkConfiguration(callback) { let message = ''; if (backupConfig.provider === 'noop') { message = 'backups.check.noop'; - } else if (backupConfig.provider === 'filesystem' && !backupConfig.externalDisk) { + } else if (backupConfig.provider === 'filesystem') { message = 'backups.check.sameDisk'; } diff --git a/src/storage/filesystem.js b/src/storage/filesystem.js index fe8da4cc5..0c4372666 100644 --- a/src/storage/filesystem.js +++ b/src/storage/filesystem.js @@ -21,6 +21,7 @@ exports = module.exports = { }; const PROVIDER_FILESYSTEM = 'filesystem'; +const PROVIDER_MOUNTPOINT = 'mountpoint'; const PROVIDER_SSHFS = 'sshfs'; const PROVIDER_CIFS = 'cifs'; const PROVIDER_NFS = 'nfs'; @@ -43,11 +44,15 @@ var assert = require('assert'), function getBackupPath(apiConfig) { assert.strictEqual(typeof apiConfig, 'object'); - if (apiConfig.provider === PROVIDER_SSHFS) return path.join(apiConfig.mountPoint, apiConfig.prefix); - if (apiConfig.provider === PROVIDER_CIFS) return path.join(apiConfig.mountPoint, apiConfig.prefix); - if (apiConfig.provider === PROVIDER_NFS) return path.join(apiConfig.mountPoint, apiConfig.prefix); - - return apiConfig.backupFolder; + switch (apiConfig.provider) { + case PROVIDER_SSHFS: + case PROVIDER_MOUNTPOINT: + case PROVIDER_NFS: + case PROVIDER_CIFS: + return path.join(apiConfig.mountPoint, apiConfig.prefix); + default: + return apiConfig.backupFolder; + } } // the du call in the function below requires root @@ -71,7 +76,7 @@ function checkPreconditions(apiConfig, dataLayout, callback) { // Check filesystem is mounted so we don't write into the actual folder on disk if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS) { if (result.mountpoint !== apiConfig.mountPoint) return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.mountPoint} is not mounted`)); - } else if (apiConfig.provider === PROVIDER_FILESYSTEM && apiConfig.externalDisk) { + } else if (apiConfig.provider === PROVIDER_MOUNTPOINT) { if (result.mountpoint === '/') return callback(new BoxError(BoxError.FS_ERROR, `${apiConfig.backupFolder} is not mounted`)); } @@ -112,7 +117,7 @@ function upload(apiConfig, backupFilePath, sourceStream, callback) { const BACKUP_UID = parseInt(process.env.SUDO_UID, 10) || process.getuid(); // sshfs and cifs handle ownership through the mount args - if (apiConfig.provider === PROVIDER_FILESYSTEM || apiConfig.provider === PROVIDER_NFS) { + if (apiConfig.provider === PROVIDER_FILESYSTEM || apiConfig.provider === PROVIDER_NFS || (apiConfig.provider === PROVIDER_MOUNTPOINT && apiConfig.chown)) { if (!safe.fs.chownSync(backupFilePath, BACKUP_UID, BACKUP_UID)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message)); if (!safe.fs.chownSync(path.dirname(backupFilePath), BACKUP_UID, BACKUP_UID)) return callback(new BoxError(BoxError.EXTERNAL_ERROR, 'Unable to chown:' + safe.error.message)); } @@ -194,7 +199,7 @@ function copy(apiConfig, oldFilePath, newFilePath) { events.emit('progress', `Copying ${oldFilePath} to ${newFilePath}`); // sshfs and cifs do not allow preserving attributes - var cpOptions = apiConfig.provider === PROVIDER_FILESYSTEM ? '-a' : '-dR'; + let cpOptions = (apiConfig.provider === PROVIDER_FILESYSTEM || apiConfig.provider === PROVIDER_NFS || (apiConfig.provider === PROVIDER_MOUNTPOINT && apiConfig.preserveAttributes)) ? '-a' : '-dR'; // this will hardlink backups saving space cpOptions += apiConfig.noHardlinks ? '' : 'l'; @@ -267,11 +272,9 @@ function testConfig(apiConfig, callback) { if (!apiConfig.backupFolder || typeof apiConfig.backupFolder !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'backupFolder must be non-empty string', { field: 'backupFolder' })); let error = validateBackupTarget(apiConfig.backupFolder); if (error) return callback(error); - - if ('externalDisk' in apiConfig && typeof apiConfig.externalDisk !== 'boolean') return callback(new BoxError(BoxError.BAD_FIELD, 'externalDisk must be boolean', { field: 'externalDisk' })); } - if (apiConfig.provider === PROVIDER_SSHFS || apiConfig.provider === PROVIDER_CIFS || apiConfig.provider === PROVIDER_NFS) { + if (apiConfig.provider === PROVIDER_MOUNTPOINT) { if (!apiConfig.mountPoint || typeof apiConfig.mountPoint !== 'string') return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be non-empty string', { field: 'mountPoint' })); let error = validateBackupTarget(apiConfig.mountPoint); if (error) return callback(error); @@ -285,10 +288,6 @@ function testConfig(apiConfig, callback) { const mounts = safe.fs.readFileSync('/proc/mounts', 'utf8'); const mountInfo = mounts.split('\n').filter(function (l) { return l.indexOf(apiConfig.mountPoint) !== -1; })[0]; if (!mountInfo) return callback(new BoxError(BoxError.BAD_FIELD, `${apiConfig.mountPoint} is not mounted`, { field: 'mountPoint' })); - - if (apiConfig.provider === PROVIDER_SSHFS && !mountInfo.split(' ').find(i => i === 'fuse.sshfs' || i === 'autofs')) return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be a "fuse.sshfs" filesystem', { field: 'mountPoint' })); - if (apiConfig.provider === PROVIDER_CIFS && !mountInfo.split(' ').find(i => i === 'cifs' || i === 'autofs')) return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be a "cifs" filesystem', { field: 'mountPoint' })); - if (apiConfig.provider === PROVIDER_NFS && !mountInfo.split(' ').find(i => i === 'nfs' || i === 'nfs4' || i === 'autofs')) return callback(new BoxError(BoxError.BAD_FIELD, 'mountPoint must be a "nfs" filesystem', { field: 'mountPoint' })); } // common checks