455 lines
10 KiB
JavaScript
455 lines
10 KiB
JavaScript
const cp = require("child_process");
|
|
const { Filesystem } = require("../utils/filesystem");
|
|
|
|
// avoid using avail,size,used as it causes hangs when the fs is stale
|
|
const FINDMNT_COMMON_OPTIONS = [
|
|
"--output",
|
|
"source,target,fstype,label,options",
|
|
"-b",
|
|
"-J",
|
|
"--nofsroot", // prevents unwanted behavior with cifs volumes
|
|
];
|
|
|
|
const DEFAULT_TIMEOUT = (() => {
|
|
const defaultValue = 30000;
|
|
if (process.env.MOUNT_DEFAULT_TIMEOUT) {
|
|
if (/^\d+$/.test(process.env.MOUNT_DEFAULT_TIMEOUT)) {
|
|
return parseInt(process.env.MOUNT_DEFAULT_TIMEOUT);
|
|
} else {
|
|
console.log("Invalid MOUNT_DEFAULT_TIMEOUT set: " + process.env.MOUNT_DEFAULT_TIMEOUT);
|
|
}
|
|
}
|
|
return defaultValue;
|
|
})()
|
|
|
|
class Mount {
|
|
constructor(options = {}) {
|
|
const mount = this;
|
|
mount.options = options;
|
|
|
|
options.paths = options.paths || {};
|
|
if (!options.paths.mount) {
|
|
options.paths.mount = "mount";
|
|
}
|
|
|
|
if (!options.paths.umount) {
|
|
options.paths.umount = "umount";
|
|
}
|
|
|
|
if (!options.paths.findmnt) {
|
|
options.paths.findmnt = "findmnt";
|
|
}
|
|
|
|
if (!options.paths.sudo) {
|
|
options.paths.sudo = "/usr/bin/sudo";
|
|
}
|
|
|
|
if (!options.paths.chroot) {
|
|
options.paths.chroot = "/usr/sbin/chroot";
|
|
}
|
|
|
|
if (!options.executor) {
|
|
options.executor = {
|
|
spawn: cp.spawn,
|
|
};
|
|
}
|
|
|
|
if (!options.filesystem) {
|
|
options.filesystem = new Filesystem();
|
|
}
|
|
}
|
|
|
|
async getFilesystemInstance() {
|
|
return this.options.filesystem;
|
|
}
|
|
|
|
/**
|
|
* findmnt --source <device> --output source,target,fstype,label,options,avail,size,used -b -J
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async deviceIsMounted(device) {
|
|
const mount = this;
|
|
const filesystem = await mount.getFilesystemInstance();
|
|
if (device.startsWith("/")) {
|
|
device = await filesystem.realpath(device);
|
|
}
|
|
|
|
let args = [];
|
|
args = args.concat(["--source", device]);
|
|
args = args.concat(FINDMNT_COMMON_OPTIONS);
|
|
let result;
|
|
|
|
try {
|
|
result = await mount.exec(mount.options.paths.findmnt, args);
|
|
} catch (err) {
|
|
// no results
|
|
if (err.code == 1) {
|
|
return false;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* findmnt --mountpoint / --output source,target,fstype,label,options,avail,size,used -b -J
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async pathIsMounted(path) {
|
|
const mount = this;
|
|
let args = [];
|
|
args = args.concat(["--mountpoint", path]);
|
|
args = args.concat(FINDMNT_COMMON_OPTIONS);
|
|
let result;
|
|
|
|
try {
|
|
result = await mount.exec(mount.options.paths.findmnt, args);
|
|
} catch (err) {
|
|
// no results
|
|
if (err.code == 1) {
|
|
return false;
|
|
} else if (
|
|
err.code == 32 &&
|
|
err.stderr &&
|
|
err.stderr.contains("No such file or directory")
|
|
) {
|
|
return false;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* findmnt --source <device> --mountpoint <path> --output source,target,fstype,label,options,avail,size,used -b -J
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async deviceIsMountedAtPath(device, path) {
|
|
const mount = this;
|
|
const filesystem = await mount.getFilesystemInstance();
|
|
if (device.startsWith("/") && !device.startsWith("//")) {
|
|
device = await filesystem.realpath(device);
|
|
}
|
|
|
|
let args = [];
|
|
args = args.concat(["--source", device]);
|
|
args = args.concat(["--mountpoint", path]);
|
|
args = args.concat(FINDMNT_COMMON_OPTIONS);
|
|
let result;
|
|
|
|
try {
|
|
result = await mount.exec(mount.options.paths.findmnt, args);
|
|
} catch (err) {
|
|
// no results
|
|
if (err.code == 1) {
|
|
return false;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* findmnt --mountpoint / --output source,target,fstype,label,options,avail,size,used -b -J
|
|
*
|
|
* @param {*} path
|
|
*/
|
|
async getMountDetails(path, extraOutputProperties = [], extraArgs = []) {
|
|
const mount = this;
|
|
let args = [];
|
|
const common_options = JSON.parse(JSON.stringify(FINDMNT_COMMON_OPTIONS));
|
|
if (extraOutputProperties.length > 0) {
|
|
common_options[1] =
|
|
common_options[1] + "," + extraOutputProperties.join(",");
|
|
}
|
|
|
|
args = args.concat(["--mountpoint", path]);
|
|
args = args.concat(common_options);
|
|
args = args.concat(extraArgs);
|
|
let result;
|
|
|
|
try {
|
|
result = await mount.exec(mount.options.paths.findmnt, args);
|
|
const parsed = JSON.parse(result.stdout);
|
|
return parsed.filesystems[0];
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* parse a mount options string into an array
|
|
*
|
|
* @param {*} options
|
|
* @returns
|
|
*/
|
|
async parseMountOptions(options) {
|
|
if (!options) {
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(options)) {
|
|
return options;
|
|
}
|
|
|
|
options = options.split(",");
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Given the set of mount options and sought after option, return true if the option is present
|
|
*
|
|
* @param {*} options
|
|
* @param {*} option
|
|
* @returns
|
|
*/
|
|
async getMountOptionPresent(options, option) {
|
|
const mount = this;
|
|
|
|
if (!Array.isArray(options)) {
|
|
options = await mount.parseMountOptions(options);
|
|
}
|
|
|
|
for (let i of options) {
|
|
let parts = i.split("=", 2);
|
|
if (parts[0] == option) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the value of the given mount option
|
|
*
|
|
* if the mount option is present by has no value null is returned
|
|
* if the mount option is NOT present undefined is returned
|
|
* is the mount option has a value that value is returned
|
|
*
|
|
* @param {*} options
|
|
* @param {*} option
|
|
* @returns
|
|
*/
|
|
async getMountOptionValue(options, option) {
|
|
const mount = this;
|
|
|
|
if (!Array.isArray(options)) {
|
|
options = await mount.parseMountOptions(options);
|
|
}
|
|
|
|
for (let i of options) {
|
|
let parts = i.split("=", 2);
|
|
if (parts[0] == option) {
|
|
if (typeof parts[1] === "undefined") {
|
|
return null;
|
|
} else {
|
|
return parts[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Get mount optsion for a given path
|
|
*
|
|
* @param {*} path
|
|
* @returns Array
|
|
*/
|
|
async getMountOptions(path) {
|
|
const mount = this;
|
|
let details = await mount.getMountDetails(path, [], ["-m"]);
|
|
|
|
return await mount.parseMountOptions(details.options);
|
|
}
|
|
|
|
/**
|
|
* Get the device (source) at the given mount point
|
|
*
|
|
* @param {*} path
|
|
*/
|
|
async getMountPointDevice(path) {
|
|
const mount = this;
|
|
const result = await mount.getMountDetails(path);
|
|
if (result.fstype == "devtmpfs") {
|
|
// dev[/sdb]
|
|
let source = "/";
|
|
source += result.source;
|
|
source = source.replace("[", "");
|
|
source = source.replace("]", "");
|
|
|
|
return source.trim();
|
|
}
|
|
return result.source.trim();
|
|
}
|
|
|
|
/**
|
|
* very specifically looking for *devices* vs *filesystems/directories* which were bind mounted
|
|
*
|
|
* @param {*} path
|
|
*/
|
|
async isBindMountedBlockDevice(path) {
|
|
const mount = this;
|
|
const filesystem = await mount.getFilesystemInstance();
|
|
|
|
const is_mounted = await mount.pathIsMounted(path);
|
|
if (!is_mounted) {
|
|
return false;
|
|
}
|
|
const mount_info = await mount.getMountDetails(path);
|
|
const is_block = await filesystem.isBlockDevice(path);
|
|
if (mount_info.fstype == "devtmpfs" && is_block) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get the filesystem type at mount point
|
|
*
|
|
* @param {*} path
|
|
*/
|
|
async getMountPointFsType(path) {
|
|
const mount = this;
|
|
const result = await mount.getMountDetails(path);
|
|
return result.fstype;
|
|
}
|
|
|
|
/**
|
|
* mount [options] <source> <directory>
|
|
*
|
|
* @param {*} source
|
|
* @param {*} target
|
|
* @param {*} options
|
|
*/
|
|
async mount(source, target, options = []) {
|
|
const mount = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
args = args.concat([source, target]);
|
|
|
|
let result;
|
|
try {
|
|
result = await mount.exec(mount.options.paths.mount, args);
|
|
return result;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* mount <operation> <mountpoint> [<target>]
|
|
*
|
|
* @param {*} source
|
|
* @param {*} target
|
|
* @param {*} options
|
|
*/
|
|
async bindMount(source, target, options = []) {
|
|
const mount = this;
|
|
let args = [];
|
|
args.push("--bind");
|
|
args = args.concat(options);
|
|
args = args.concat([source, target]);
|
|
|
|
let result;
|
|
try {
|
|
result = await mount.exec(mount.options.paths.mount, args);
|
|
return result;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* umount [options] <source> | <directory>
|
|
*
|
|
* @param {*} target
|
|
* @param {*} options
|
|
*/
|
|
async umount(target, options = []) {
|
|
const mount = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
args.push(target);
|
|
|
|
try {
|
|
await mount.exec(mount.options.paths.umount, args);
|
|
} catch (err) {
|
|
if (err.code == 32) {
|
|
return true;
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
exec(command, args, options = {}) {
|
|
if (!options.hasOwnProperty("timeout")) {
|
|
options.timeout = DEFAULT_TIMEOUT;
|
|
}
|
|
|
|
const mount = this;
|
|
args = args || [];
|
|
|
|
if (mount.options.sudo) {
|
|
args.unshift(command);
|
|
command = mount.options.paths.sudo;
|
|
}
|
|
// https://regex101.com/r/FHIbcw/3
|
|
// replace password=foo with password=redacted
|
|
// (?<=password=)(?:([\"'])(?:\\\1|.)*?\1|[^,\s]+)
|
|
const regex = /(?<=password=)(?:([\"'])(?:\\\1|.)*?\1|[^,\s]+)/gi;
|
|
const cleansedLog = `${command} ${args.join(" ")}`.replace(
|
|
regex,
|
|
"redacted"
|
|
);
|
|
|
|
console.log("executing mount command: %s", cleansedLog);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const child = mount.options.executor.spawn(command, args, options);
|
|
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
child.stdout.on("data", function (data) {
|
|
stdout = stdout + data;
|
|
});
|
|
|
|
child.stderr.on("data", function (data) {
|
|
stderr = stderr + data;
|
|
});
|
|
|
|
child.on("close", function (code) {
|
|
const result = { code, stdout, stderr, timeout: false };
|
|
|
|
// timeout scenario
|
|
if (code === null) {
|
|
result.timeout = true;
|
|
reject(result);
|
|
}
|
|
|
|
if (code) {
|
|
reject(result);
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports.Mount = Mount;
|