diff --git a/migrations/20190114230513-apps-add-dataDir.js b/migrations/20190114230513-apps-add-dataDir.js new file mode 100644 index 000000000..16b15d148 --- /dev/null +++ b/migrations/20190114230513-apps-add-dataDir.js @@ -0,0 +1,15 @@ +'use strict'; + +exports.up = function(db, callback) { + db.runSql('ALTER TABLE apps ADD COLUMN dataDir VARCHAR(256) UNIQUE', function (error) { + if (error) console.error(error); + callback(error); + }); +}; + +exports.down = function(db, callback) { + db.runSql('ALTER TABLE apps DROP COLUMN dataDir', function (error) { + if (error) console.error(error); + callback(error); + }); +}; diff --git a/setup/start/sudoers b/setup/start/sudoers index e08a09915..8d6a47ba5 100644 --- a/setup/start/sudoers +++ b/setup/start/sudoers @@ -4,6 +4,12 @@ Defaults !syslog Defaults!/home/yellowtent/box/src/scripts/rmvolume.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmvolume.sh +Defaults!/home/yellowtent/box/src/scripts/mvvolume.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mvvolume.sh + +Defaults!/home/yellowtent/box/src/scripts/mkdirvolume.sh env_keep="HOME BOX_ENV" +yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/mkdirvolume.sh + Defaults!/home/yellowtent/box/src/scripts/rmaddondir.sh env_keep="HOME BOX_ENV" yellowtent ALL=(root) NOPASSWD: /home/yellowtent/box/src/scripts/rmaddondir.sh diff --git a/src/addons.js b/src/addons.js index 0841fb4c4..65473031d 100644 --- a/src/addons.js +++ b/src/addons.js @@ -33,6 +33,7 @@ exports = module.exports = { var accesscontrol = require('./accesscontrol.js'), appdb = require('./appdb.js'), + apps = require('./apps.js'), assert = require('assert'), async = require('async'), clients = require('./clients.js'), @@ -728,7 +729,7 @@ function setupLocalStorage(app, options, callback) { debugApp(app, 'setupLocalStorage'); - const volumeDataDir = path.join(paths.APPS_DATA_DIR, app.id, 'data'); + const volumeDataDir = apps.getDataDir(app, app.dataDir); // if you change the name, you have to change getMountsSync docker.createVolume(app, `${app.id}-localstorage`, volumeDataDir, callback); diff --git a/src/appdb.js b/src/appdb.js index 41fe7087f..4877053b8 100644 --- a/src/appdb.js +++ b/src/appdb.js @@ -69,7 +69,8 @@ var APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationSta 'apps.health', 'apps.containerId', 'apps.manifestJson', 'apps.httpPort', 'subdomains.subdomain AS location', 'subdomains.domain', 'apps.accessRestrictionJson', 'apps.restoreConfigJson', 'apps.oldConfigJson', 'apps.updateConfigJson', 'apps.memoryLimit', 'apps.xFrameOptions', 'apps.sso', 'apps.debugModeJson', 'apps.robotsTxt', 'apps.enableBackup', - 'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', 'apps.ts' ].join(','); + 'apps.creationTime', 'apps.updateTime', 'apps.ownerId', 'apps.mailboxName', 'apps.enableAutomaticUpdate', + 'apps.dataDir', 'apps.ts' ].join(','); var PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(','); @@ -139,6 +140,9 @@ function postProcess(result) { for (let i = 0; i < envNames.length; i++) { // NOTE: envNames is [ null ] when env of an app is empty if (envNames[i]) result.env[envNames[i]] = envValues[i]; } + + // in the db, we store dataDir as unique/nullable + result.dataDir = result.dataDir || ''; } function get(id, callback) { diff --git a/src/apps.js b/src/apps.js index 5ed439393..3e9b90fa4 100644 --- a/src/apps.js +++ b/src/apps.js @@ -38,6 +38,7 @@ exports = module.exports = { configureInstalledApps: configureInstalledApps, getAppConfig: getAppConfig, + getDataDir: getDataDir, downloadFile: downloadFile, uploadFile: uploadFile, @@ -301,6 +302,26 @@ function validateEnv(env) { return null; } +function validateDataDir(dataDir) { + if (!dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir cannot be empty'); + + if (path.resolve(dataDir) !== dataDir) return new AppsError(AppsError.BAD_FIELD, 'dataDir must be an absolute path'); + + // nfs shares will have the directory mounted already + let stat = safe.fs.lstatSync(dataDir); + if (stat) { + if (!stat.isDirectory()) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not a directory`); + let entries = safe.fs.readdirSync(dataDir); + if (!entries) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} could not be listed`); + if (entries.length !== 0) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} is not empty`); + } + + // tgz backup logic relies on path not overlapping because it recurses + if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new AppsError(AppsError.BAD_FIELD, `dataDir ${dataDir} cannot be inside apps data`); + + return null; +} + function getDuplicateErrorDetails(location, portBindings, error) { assert.strictEqual(typeof location, 'string'); assert.strictEqual(typeof portBindings, 'object'); @@ -337,17 +358,22 @@ function getAppConfig(app) { robotsTxt: app.robotsTxt, sso: app.sso, alternateDomains: app.alternateDomains || [], - env: app.env + env: app.env, + dataDir: app.dataDir }; } +function getDataDir(app, dataDir) { + return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data'); +} + function removeInternalFields(app) { return _.pick(app, 'id', 'appStoreId', 'installationState', 'installationProgress', 'runState', 'health', 'location', 'domain', 'fqdn', 'mailboxName', 'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'xFrameOptions', 'sso', 'debugMode', 'robotsTxt', 'enableBackup', 'creationTime', 'updateTime', 'ts', - 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate'); + 'alternateDomains', 'ownerId', 'env', 'enableAutomaticUpdate', 'dataDir'); } function removeRestrictedFields(app) { @@ -747,6 +773,12 @@ function configure(appId, data, user, auditSource, callback) { if (error) return callback(error); } + if ('dataDir' in data) { + error = validateDataDir(data.dataDir); + if (error) return callback(error); + values.dataDir = data.dataDir; + } + domains.get(domain, function (error, domainObject) { if (error && error.reason === DomainsError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND, 'No such domain')); if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, 'Could not get domain info:' + error.message)); diff --git a/src/apptask.js b/src/apptask.js index 0e83b8c10..980557b7b 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -52,6 +52,7 @@ var addons = require('./addons.js'), var COLLECTD_CONFIG_EJS = fs.readFileSync(__dirname + '/collectd.config.ejs', { encoding: 'utf8' }), CONFIGURE_COLLECTD_CMD = path.join(__dirname, 'scripts/configurecollectd.sh'), + MV_VOLUME_CMD = path.join(__dirname, 'scripts/mvvolume.sh'), LOGROTATE_CONFIG_EJS = fs.readFileSync(__dirname + '/logrotate.ejs', { encoding: 'utf8' }), CONFIGURE_LOGROTATE_CMD = path.join(__dirname, 'scripts/configurelogrotate.sh'); @@ -469,6 +470,19 @@ function waitForDnsPropagation(app, callback) { }); } +function migrateDataDir(app, sourceDir, callback) { + assert.strictEqual(typeof app, 'object'); + assert.strictEqual(typeof sourceDir, 'string'); + assert.strictEqual(typeof callback, 'function'); + + let resolvedSourceDir = apps.getDataDir(app, sourceDir); + let resolvedTargetDir = apps.getDataDir(app, app.dataDir); + + debug(`migrateDataDir: migrating data from ${resolvedSourceDir} to ${resolvedTargetDir}`); + + shell.sudo('migrateDataDir', [ MV_VOLUME_CMD, resolvedSourceDir, resolvedTargetDir ], {}, callback); +} + // Ordering is based on the following rationale: // - configure nginx, icon, oauth // - register subdomain. @@ -605,7 +619,8 @@ function configure(app, callback) { assert.strictEqual(typeof callback, 'function'); // oldConfig can be null during an infra update - var locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn); + const locationChanged = app.oldConfig && (app.oldConfig.fqdn !== app.fqdn); + const dataDirChanged = app.oldConfig && (app.oldConfig.dataDir !== app.dataDir); async.series([ updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }), @@ -643,6 +658,13 @@ function configure(app, callback) { updateApp.bind(null, app, { installationProgress: '50, Setting up addons' }), addons.setupAddons.bind(null, app, app.manifest.addons), + // migrate dataDir + function (next) { + if (!dataDirChanged) return next(); + + migrateDataDir(app, app.oldConfig.dataDir, next); + }, + updateApp.bind(null, app, { installationProgress: '60, Creating container' }), createContainer.bind(null, app), diff --git a/src/backups.js b/src/backups.js index 98c77c925..cc57bf123 100644 --- a/src/backups.js +++ b/src/backups.js @@ -260,16 +260,23 @@ function createWriteStream(destFile, key) { } } -function tarPack(sourceDir, key, callback) { - assert.strictEqual(typeof sourceDir, 'string'); +function tarPack(dataLayout, key, callback) { + assert(Array.isArray(dataLayout), 'dataLayout must be a string'); assert(key === null || typeof key === 'string'); assert.strictEqual(typeof callback, 'function'); + let regexps = dataLayout.map((l) => new RegExp('^' + l.localDir + '/?')); + var pack = tar.pack('/', { dereference: false, // pack the symlink and not what it points to - entries: [ sourceDir ], + entries: dataLayout.map((e) => e.localDir), map: function(header) { - header.name = header.name.replace(new RegExp('^' + sourceDir + '(/?)'), '.$1'); // make paths relative + for (let i = 0; i < dataLayout.length; i++) { + if (!header.name.match(regexps[i])) continue; + const maybeSlash = dataLayout[i].remoteDir ? '/' : ''; + header.name = header.name.replace(regexps[i], './' + dataLayout[i].remoteDir + maybeSlash); // make paths relative + break; + } return header; }, strict: false // do not error for unknown types (skip fifo, char/block devices) @@ -381,14 +388,14 @@ function saveFsMetadata(appDataDir, callback) { } // this function is called via backupupload (since it needs root to traverse app's directory) -function upload(backupId, format, dataDir, progressCallback, callback) { +function upload(backupId, format, dataLayout, progressCallback, callback) { assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof format, 'string'); - assert.strictEqual(typeof dataDir, 'string'); + assert(Array.isArray(dataLayout), 'dataLayout should be an array'); assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); - debug(`upload: id ${backupId} format ${format} dataDir ${dataDir}`); + debug(`upload: id ${backupId} format ${format} dataLayout ${JSON.stringify(dataLayout)}`); settings.getBackupConfig(function (error, backupConfig) { if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); @@ -397,7 +404,7 @@ function upload(backupId, format, dataDir, progressCallback, callback) { async.retry({ times: 5, interval: 20000 }, function (retryCallback) { retryCallback = once(retryCallback); // protect again upload() erroring much later after tar stream error - tarPack(dataDir, backupConfig.key || null, function (error, tarStream) { + tarPack(dataLayout, backupConfig.key || null, function (error, tarStream) { if (error) return retryCallback(error); tarStream.on('progress', function(progress) { @@ -411,6 +418,7 @@ function upload(backupId, format, dataDir, progressCallback, callback) { }); }, callback); } else { + const dataDir = dataLayout[0].localDir; // FIXME: make rsync format support data layout async.series([ saveFsMetadata.bind(null, dataDir), sync.bind(null, backupConfig, backupId, dataDir, progressCallback) @@ -419,15 +427,28 @@ function upload(backupId, format, dataDir, progressCallback, callback) { }); } -function tarExtract(inStream, destination, key, callback) { +function tarExtract(inStream, dataLayout, key, callback) { assert.strictEqual(typeof inStream, 'object'); - assert.strictEqual(typeof destination, 'string'); + assert(Array.isArray(dataLayout), 'dataLayout should be an array'); assert(key === null || typeof key === 'string'); assert.strictEqual(typeof callback, 'function'); + // assumes the patterns in layout are reverse priorotized + let regexps = dataLayout.reverse().map((l) => new RegExp('^\\.' + l.remoteDir + '/?')); + var gunzip = zlib.createGunzip({}); var ps = progressStream({ time: 10000 }); // display a progress every 10 seconds - var extract = tar.extract(destination); + var extract = tar.extract('/', { + map: function (header) { + for (let i = 0; i < dataLayout.length; i++) { + if (!header.name.match(regexps[i])) continue; + header.name = header.name.replace(regexps[i], dataLayout[i].localDir + '/'); // make paths absolute + break; + } + + return header; + } + }); const emitError = once((error) => ps.emit('error', error)); @@ -539,21 +560,21 @@ function downloadDir(backupConfig, backupFilePath, destDir, progressCallback, ca api(backupConfig.provider).listDir(backupConfig, backupFilePath, 1000, function (entries, done) { // https://www.digitalocean.com/community/questions/rate-limiting-on-spaces?answer=40441 - const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 300 : 100); + const concurrency = backupConfig.downloadConcurrency || (backupConfig.provider === 's3' ? 30 : 10); async.eachLimit(entries, concurrency, downloadFile, done); }, callback); } -function download(backupConfig, backupId, format, dataDir, progressCallback, callback) { +function download(backupConfig, backupId, format, dataLayout, progressCallback, callback) { assert.strictEqual(typeof backupConfig, 'object'); assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof format, 'string'); - assert.strictEqual(typeof dataDir, 'string'); + assert(Array.isArray(dataLayout), 'dataLayout must be a string'); assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); - debug(`download - Downloading ${backupId} of format ${format} to ${dataDir}`); + debug(`download - Downloading ${backupId} of format ${format} to ${JSON.stringify(dataLayout)}`); const backupFilePath = getBackupFilePath(backupConfig, backupId, format); @@ -562,7 +583,7 @@ function download(backupConfig, backupId, format, dataDir, progressCallback, cal if (error) return callback(error); async.retry({ times: 5, interval: 20000 }, function (retryCallback) { - tarExtract(sourceStream, dataDir, backupConfig.key || null, function (error, ps) { + tarExtract(sourceStream, dataLayout, backupConfig.key || null, function (error, ps) { if (error) return retryCallback(error); ps.on('progress', function (progress) { @@ -576,6 +597,7 @@ function download(backupConfig, backupId, format, dataDir, progressCallback, cal }, callback); }); } else { + let dataDir = dataLayout[0].localDir; // FIXME: make rsync format support data layout downloadDir(backupConfig, backupFilePath, dataDir, progressCallback, function (error) { if (error) return callback(error); @@ -590,7 +612,9 @@ function restore(backupConfig, backupId, progressCallback, callback) { assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); - download(backupConfig, backupId, backupConfig.format, paths.BOX_DATA_DIR, progressCallback, function (error) { + const dataLayout = [ { localDir: paths.BOX_DATA_DIR, remoteDir: '' } ]; + + download(backupConfig, backupId, backupConfig.format, dataLayout, progressCallback, function (error) { if (error) return callback(error); debug('restore: download completed, importing database'); @@ -613,6 +637,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb assert.strictEqual(typeof callback, 'function'); var appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id)); + const dataLayout = [ { localDir: appDataDir, remoteDir: '' } ]; var startTime = new Date(); @@ -620,7 +645,7 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb if (error) return callback(new BackupsError(BackupsError.INTERNAL_ERROR, error)); async.series([ - download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, appDataDir, progressCallback), + download.bind(null, backupConfig, restoreConfig.backupId, restoreConfig.backupFormat, dataLayout, progressCallback), addons.restoreAddons.bind(null, app, addonsToRestore) ], function (error) { debug('restoreApp: time: %s', (new Date() - startTime)/1000); @@ -630,16 +655,16 @@ function restoreApp(app, addonsToRestore, restoreConfig, progressCallback, callb }); } -function runBackupUpload(backupId, format, dataDir, progressCallback, callback) { +function runBackupUpload(backupId, format, dataLayout, progressCallback, callback) { assert.strictEqual(typeof backupId, 'string'); assert.strictEqual(typeof format, 'string'); - assert.strictEqual(typeof dataDir, 'string'); + assert(Array.isArray(dataLayout), 'dataLayout should be an array'); assert.strictEqual(typeof progressCallback, 'function'); assert.strictEqual(typeof callback, 'function'); let result = ''; - shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, dataDir ], { preserveEnv: true, ipc: true }, function (error) { + shell.sudo(`backup-${backupId}`, [ BACKUP_UPLOAD_CMD, backupId, format, JSON.stringify(dataLayout) ], { preserveEnv: true, ipc: true }, function (error) { if (error && (error.code === null /* signal */ || (error.code !== 0 && error.code !== 50))) { // backuptask crashed return callback(new BackupsError(BackupsError.INTERNAL_ERROR, 'Backuptask crashed')); } else if (error && error.code === 50) { // exited with error @@ -702,7 +727,8 @@ function uploadBoxSnapshot(backupConfig, progressCallback, callback) { const boxDataDir = safe.fs.realpathSync(paths.BOX_DATA_DIR); if (!boxDataDir) return callback(safe.error); - runBackupUpload('snapshot/box', backupConfig.format, boxDataDir, progressCallback, function (error) { + const dataLayout = [ { localDir: boxDataDir, remoteDir: '' } ]; + runBackupUpload('snapshot/box', backupConfig.format, dataLayout, progressCallback, function (error) { if (error) return callback(error); debug('uploadBoxSnapshot: time: %s secs', (new Date() - startTime)/1000); @@ -878,7 +904,9 @@ function uploadAppSnapshot(backupConfig, app, progressCallback, callback) { const appDataDir = safe.fs.realpathSync(path.join(paths.APPS_DATA_DIR, app.id)); if (!appDataDir) return callback(safe.error); - runBackupUpload(backupId, backupConfig.format, appDataDir, progressCallback, function (error) { + let dataLayout = [ { localDir: appDataDir, remoteDir: '' } ]; + if (app.dataDir) dataLayout.push({ localDir: app.dataDir, remoteDir: 'data' }); + runBackupUpload(backupId, backupConfig.format, dataLayout, progressCallback, function (error) { if (error) return callback(error); debugApp(app, 'uploadAppSnapshot: %s done time: %s secs', backupId, (new Date() - startTime)/1000); diff --git a/src/docker.js b/src/docker.js index 0c5a4ed40..d5ec90770 100644 --- a/src/docker.js +++ b/src/docker.js @@ -54,7 +54,6 @@ var addons = require('./addons.js'), config = require('./config.js'), constants = require('./constants.js'), debug = require('debug')('box:docker.js'), - mkdirp = require('mkdirp'), once = require('once'), path = require('path'), shell = require('./shell.js'), @@ -62,7 +61,8 @@ var addons = require('./addons.js'), util = require('util'), _ = require('underscore'); -const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh'); +const RMVOLUME_CMD = path.join(__dirname, 'scripts/rmvolume.sh'), + MKDIRVOLUME_CMD = path.join(__dirname, 'scripts/mkdirvolume.sh'); function DockerError(reason, errorOrMessage) { assert.strictEqual(typeof reason, 'string'); @@ -537,7 +537,8 @@ function createVolume(app, name, volumeDataDir, callback) { }, }; - mkdirp(volumeDataDir, function (error) { + // requires sudo because the path can be outside appsdata + shell.sudo('createVolume', [ MKDIRVOLUME_CMD, volumeDataDir ], {}, function (error) { if (error) return callback(new Error(`Error creating app data dir: ${error.message}`)); docker.createVolume(volumeOptions, function (error) { diff --git a/src/routes/apps.js b/src/routes/apps.js index 955e2b101..db853fd53 100644 --- a/src/routes/apps.js +++ b/src/routes/apps.js @@ -210,6 +210,8 @@ function configureApp(req, res, next) { if (Object.keys(data.env).some(function (key) { return typeof data.env[key] !== 'string'; })) return next(new HttpError(400, 'env must contain values as strings')); } + if ('dataDir' in data && typeof data.dataDir !== 'string') return next(new HttpError(400, 'dataDir must be a string')); + debug('Configuring app id:%s data:%j', req.params.id, data); apps.configure(req.params.id, data, req.user, auditSource(req), function (error) { diff --git a/src/scripts/backupupload.js b/src/scripts/backupupload.js index 212ead40e..fae3081fc 100755 --- a/src/scripts/backupupload.js +++ b/src/scripts/backupupload.js @@ -21,11 +21,11 @@ function initialize(callback) { } // Main process starts here -var backupId = process.argv[2]; -var format = process.argv[3]; -var dataDir = process.argv[4]; +const backupId = process.argv[2]; +const format = process.argv[3]; +const dataLayout = JSON.parse(process.argv[4]); -debug(`Backing up ${dataDir} to ${backupId}`); +debug(`Backing up ${JSON.stringify(dataLayout)} to ${backupId}`); process.on('SIGTERM', function () { process.exit(0); @@ -40,7 +40,7 @@ process.on('disconnect', function () { initialize(function (error) { if (error) throw error; - backups.upload(backupId, format, dataDir, (progress) => process.send(progress), function resultHandler(error) { + backups.upload(backupId, format, dataLayout, (progress) => process.send(progress), function resultHandler(error) { if (error) debug('upload completed with error', error); debug('upload completed'); diff --git a/src/scripts/mkdirvolume.sh b/src/scripts/mkdirvolume.sh new file mode 100755 index 000000000..b17e933f8 --- /dev/null +++ b/src/scripts/mkdirvolume.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -eu -o pipefail + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "No arguments supplied" + exit 1 +fi + +if [[ "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +volume_dir="$1" + +mkdir -p "${volume_dir}" + diff --git a/src/scripts/mvvolume.sh b/src/scripts/mvvolume.sh new file mode 100755 index 000000000..9152a91d6 --- /dev/null +++ b/src/scripts/mvvolume.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -eu -o pipefail + +if [[ ${EUID} -ne 0 ]]; then + echo "This script should be run as root." > /dev/stderr + exit 1 +fi + +if [[ $# -eq 0 ]]; then + echo "No arguments supplied" + exit 1 +fi + +if [[ "$1" == "--check" ]]; then + echo "OK" + exit 0 +fi + +source_dir="$1" +target_dir="$2" + +if [[ "${BOX_ENV}" == "test" ]]; then + # be careful not to nuke some random directory when testing + [[ "${source_dir}" != *"./cloudron_test/"* ]] && exit 1 + [[ "${target_dir}" != *"./cloudron_test/"* ]] && exit 1 +fi + +# copy and remove - this way if the copy fails, the original is intact +# the find logic is so that move to a subdir works (and we also move hidden files) +find "${source_dir}" -maxdepth 1 -mindepth 1 -not -wholename "${target_dir}" -exec cp -ar '{}' "${target_dir}" \; +find "${source_dir}" -maxdepth 1 -mindepth 1 -not -wholename "${target_dir}" -exec rm -rf '{}' \; +# this will fail if target is a subdir +rmdir "${source_dir}" || true + diff --git a/src/test/apps-test.js b/src/test/apps-test.js index 9ec866843..5cd31e620 100644 --- a/src/test/apps-test.js +++ b/src/test/apps-test.js @@ -118,7 +118,8 @@ describe('Apps', function () { ownerId: USER_0.id, env: { 'CUSTOM_KEY': 'CUSTOM_VALUE' - } + }, + dataDir: '' }; var APP_1 = { @@ -135,7 +136,8 @@ describe('Apps', function () { accessRestriction: { users: [ 'someuser' ], groups: [ GROUP_0.id ] }, memoryLimit: 0, ownerId: USER_0.id, - env: {} + env: {}, + dataDir: '' }; var APP_2 = { @@ -154,7 +156,8 @@ describe('Apps', function () { robotsTxt: null, sso: false, ownerId: USER_0.id, - env: {} + env: {}, + dataDir: '' }; before(function (done) { diff --git a/src/test/checkInstall b/src/test/checkInstall index 13fb0c3fc..4f7e548e4 100755 --- a/src/test/checkInstall +++ b/src/test/checkInstall @@ -10,6 +10,8 @@ sudo -k || sudo --reset-timestamp # checks if all scripts are sudo access scripts=("${SOURCE_DIR}/src/scripts/rmvolume.sh" \ + "${SOURCE_DIR}/src/scripts/mvvolume.sh" \ + "${SOURCE_DIR}/src/scripts/mkdirvolume.sh" \ "${SOURCE_DIR}/src/scripts/rmaddondir.sh" \ "${SOURCE_DIR}/src/scripts/reloadnginx.sh" \ "${SOURCE_DIR}/src/scripts/reboot.sh" \ diff --git a/src/test/database-test.js b/src/test/database-test.js index f0d728194..f087360e8 100644 --- a/src/test/database-test.js +++ b/src/test/database-test.js @@ -380,7 +380,8 @@ describe('database', function () { ownerId: USER_0.id, env: {}, mailboxName: 'talktome', - enableAutomaticUpdate: true + enableAutomaticUpdate: true, + dataDir: '' }; it('cannot delete referenced domain', function (done) { @@ -948,7 +949,8 @@ describe('database', function () { 'CUSTOM_KEY': 'CUSTOM_VALUE' }, mailboxName: 'talktome', - enableAutomaticUpdate: true + enableAutomaticUpdate: true, + dataDir: '' }; var APP_1 = { @@ -978,7 +980,8 @@ describe('database', function () { alternateDomains: [], env: {}, mailboxName: 'callme', - enableAutomaticUpdate: true + enableAutomaticUpdate: true, + dataDir: '' }; before(function (done) {