When adding a volume, this comes in mountOptions. The hostPath in the database is the computed host path.
177 lines
6.5 KiB
JavaScript
177 lines
6.5 KiB
JavaScript
'use strict';
|
|
|
|
exports = module.exports = {
|
|
add,
|
|
get,
|
|
del,
|
|
list,
|
|
remount,
|
|
getStatus,
|
|
removePrivateFields,
|
|
|
|
mountAll
|
|
};
|
|
|
|
const assert = require('assert'),
|
|
BoxError = require('./boxerror.js'),
|
|
constants = require('./constants.js'),
|
|
database = require('./database.js'),
|
|
debug = require('debug')('box:volumes'),
|
|
eventlog = require('./eventlog.js'),
|
|
mounts = require('./mounts.js'),
|
|
path = require('path'),
|
|
paths = require('./paths.js'),
|
|
safe = require('safetydance'),
|
|
services = require('./services.js'),
|
|
uuid = require('uuid');
|
|
|
|
const VOLUMES_FIELDS = [ 'id', 'name', 'hostPath', 'creationTime', 'mountType', 'mountOptionsJson' ].join(',');
|
|
|
|
function postProcess(result) {
|
|
assert.strictEqual(typeof result, 'object');
|
|
|
|
result.mountOptions = safe.JSON.parse(result.mountOptionsJson) || {};
|
|
delete result.mountOptionsJson;
|
|
|
|
return result;
|
|
}
|
|
|
|
function validateName(name) {
|
|
assert.strictEqual(typeof name, 'string');
|
|
|
|
if (!/^[-\w^&'@{}[\],$=!#().%+~ ]+$/.test(name)) return new BoxError(BoxError.BAD_FIELD, 'Invalid name');
|
|
|
|
return null;
|
|
}
|
|
|
|
function validateHostPath(hostPath, mountType) {
|
|
assert.strictEqual(typeof hostPath, 'string');
|
|
assert.strictEqual(typeof mountType, 'string');
|
|
|
|
if (path.normalize(hostPath) !== hostPath) return new BoxError(BoxError.BAD_FIELD, 'hostPath must contain a normalized path');
|
|
if (!path.isAbsolute(hostPath)) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be an absolute path');
|
|
|
|
if (hostPath === '/') return new BoxError(BoxError.BAD_FIELD, 'hostPath cannot be /');
|
|
|
|
if (!hostPath.endsWith('/')) hostPath = hostPath + '/'; // ensure trailing slash for the prefix matching to work
|
|
const allowedPaths = [ '/mnt/', '/media/', '/srv/', '/opt/' ];
|
|
|
|
if (!allowedPaths.some(p => hostPath.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, 'hostPath must be under /mnt, /media, /opt or /srv');
|
|
|
|
if (!constants.TEST) { // we expect user to have already mounted this
|
|
const stat = safe.fs.lstatSync(hostPath);
|
|
if (!stat) return new BoxError(BoxError.BAD_FIELD, 'hostPath does not exist. Please create it on the server first');
|
|
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, 'hostPath is not a directory');
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function add(volume, auditSource) {
|
|
assert.strictEqual(typeof volume, 'object');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
const {name, mountType, mountOptions} = volume;
|
|
|
|
let error = validateName(name);
|
|
if (error) throw error;
|
|
|
|
error = mounts.validateMountOptions(mountType, mountOptions);
|
|
if (error) throw error;
|
|
|
|
const id = uuid.v4().replace(/-/g, ''); // to make systemd mount file names more readable
|
|
|
|
let hostPath;
|
|
if (mountType === 'mountpoint' || mountType === 'filesystem') {
|
|
error = validateHostPath(mountOptions.hostPath, mountType);
|
|
if (error) throw error;
|
|
hostPath = mountOptions.hostPath;
|
|
} else {
|
|
hostPath = path.join(paths.VOLUMES_MOUNT_DIR, id);
|
|
const mount = { name, hostPath, mountType, mountOptions };
|
|
await mounts.tryAddMount(mount, { timeout: 10 }); // 10 seconds
|
|
}
|
|
|
|
[error] = await safe(database.query('INSERT INTO volumes (id, name, hostPath, mountType, mountOptionsJson) VALUES (?, ?, ?, ?, ?)', [ id, name, hostPath, mountType, JSON.stringify(mountOptions) ]));
|
|
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('name') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'name already exists');
|
|
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('hostPath') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'hostPath already exists');
|
|
if (error && error.code === 'ER_DUP_ENTRY' && error.sqlMessage.indexOf('PRIMARY') !== -1) throw new BoxError(BoxError.ALREADY_EXISTS, 'id already exists');
|
|
if (error) throw error;
|
|
|
|
await eventlog.add(eventlog.ACTION_VOLUME_ADD, auditSource, { id, name, hostPath });
|
|
// in theory, we only need to do this mountpoint volumes. but for some reason a restart is required to detect new "mounts"
|
|
safe(services.rebuildService('sftp', auditSource), { debug });
|
|
|
|
return id;
|
|
}
|
|
|
|
async function remount(volume, auditSource) {
|
|
assert.strictEqual(typeof volume, 'object');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
await eventlog.add(eventlog.ACTION_VOLUME_REMOUNT, auditSource, { volume });
|
|
|
|
if (!mounts.isManagedProvider(volume.mountType)) throw new BoxError(BoxError.NOT_SUPPORTED, 'Volume does not support remount');
|
|
|
|
await mounts.remount(volume);
|
|
}
|
|
|
|
async function getStatus(volume) {
|
|
assert.strictEqual(typeof volume, 'object');
|
|
|
|
return await mounts.getStatus(volume.mountType, volume.hostPath); // { state, message }
|
|
}
|
|
|
|
function removePrivateFields(volume) {
|
|
const newVolume = Object.assign({}, volume);
|
|
|
|
if (newVolume.mountType === 'sshfs') {
|
|
newVolume.mountOptions.privateKey = constants.SECRET_PLACEHOLDER;
|
|
} else if (newVolume.mountType === 'cifs') {
|
|
newVolume.mountOptions.password = constants.SECRET_PLACEHOLDER;
|
|
}
|
|
|
|
return newVolume;
|
|
}
|
|
|
|
async function get(id) {
|
|
assert.strictEqual(typeof id, 'string');
|
|
|
|
const result = await database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes WHERE id=?`, [ id ]);
|
|
if (result.length === 0) return null;
|
|
|
|
return postProcess(result[0]);
|
|
}
|
|
|
|
async function list() {
|
|
const results = await database.query(`SELECT ${VOLUMES_FIELDS} FROM volumes ORDER BY name`);
|
|
results.forEach(postProcess);
|
|
return results;
|
|
}
|
|
|
|
async function del(volume, auditSource) {
|
|
assert.strictEqual(typeof volume, 'object');
|
|
assert.strictEqual(typeof auditSource, 'object');
|
|
|
|
const [error, result] = await safe(database.query('DELETE FROM volumes WHERE id=?', [ volume.id ]));
|
|
if (error && error.code === 'ER_ROW_IS_REFERENCED_2') throw new BoxError(BoxError.CONFLICT, 'Volume is in use');
|
|
if (error) throw error;
|
|
if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Volume not found');
|
|
|
|
await eventlog.add(eventlog.ACTION_VOLUME_REMOVE, auditSource, { volume });
|
|
|
|
if (volume.mountType === 'mountpoint' || volume.mountType === 'filesystem') {
|
|
safe(services.rebuildService('sftp', auditSource), { debug });
|
|
} else {
|
|
await safe(mounts.removeMount(volume));
|
|
}
|
|
}
|
|
|
|
async function mountAll() {
|
|
debug('mountAll: mouting all volumes');
|
|
|
|
for (const volume of await list()) {
|
|
await mounts.tryAddMount(volume, { timeout: 10, skipCleanup: true }); // have to wait to avoid race with apptask
|
|
}
|
|
}
|