472 lines
9.7 KiB
JavaScript
472 lines
9.7 KiB
JavaScript
const cp = require("child_process");
|
|
const fs = require("fs");
|
|
|
|
/**
|
|
* https://github.com/kubernetes/kubernetes/tree/master/pkg/util/mount
|
|
* https://github.com/kubernetes/kubernetes/blob/master/pkg/util/mount/mount_linux.go
|
|
*/
|
|
class Filesystem {
|
|
constructor(options = {}) {
|
|
const filesystem = this;
|
|
filesystem.options = options;
|
|
|
|
options.paths = options.paths || {};
|
|
|
|
if (!options.paths.sudo) {
|
|
options.paths.sudo = "/usr/bin/sudo";
|
|
}
|
|
|
|
if (!options.timeout) {
|
|
options.timeout = 10 * 60 * 1000;
|
|
}
|
|
|
|
if (!options.executor) {
|
|
options.executor = {
|
|
spawn: cp.spawn
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempt to discover if device is a block device
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async isBlockDevice(device) {
|
|
const filesystem = this;
|
|
|
|
if (!device.startsWith("/")) {
|
|
return false;
|
|
}
|
|
const device_path = await filesystem.realpath(device);
|
|
const blockdevices = await filesystem.getAllBlockDevices();
|
|
|
|
return blockdevices.some(i => {
|
|
if (i.path == device_path) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* create symlink
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async symlink(target, link, options = []) {
|
|
const filesystem = this;
|
|
let args = ["-s"];
|
|
args = args.concat(options);
|
|
args = args.concat([target, link]);
|
|
|
|
try {
|
|
await filesystem.exec("ln", args);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* create symlink
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async rm(options = []) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
|
|
try {
|
|
await filesystem.exec("rm", args);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* touch a path
|
|
* @param {*} path
|
|
*/
|
|
async touch(path, options = []) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
args.push(path);
|
|
|
|
try {
|
|
await filesystem.exec("touch", args);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* touch a path
|
|
* @param {*} path
|
|
*/
|
|
async dirname(path) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args.push(path);
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("dirname", args);
|
|
return result.stdout.trim();
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* lsblk -a -b -l -J -O
|
|
*/
|
|
async getAllBlockDevices() {
|
|
const filesystem = this;
|
|
let args = ["-a", "-b", "-l", "-J", "-O"];
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("lsblk", args);
|
|
const parsed = JSON.parse(result.stdout);
|
|
return parsed.blockdevices;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* lsblk -a -b -l -J -O
|
|
*/
|
|
async getBlockDevice(device) {
|
|
const filesystem = this;
|
|
device = await filesystem.realpath(device);
|
|
let args = ["-a", "-b", "-l", "-J", "-O"];
|
|
args.push(device);
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("lsblk", args);
|
|
const parsed = JSON.parse(result.stdout);
|
|
return parsed.blockdevices[0];
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* blkid -p -o export <device>
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async deviceIsFormatted(device) {
|
|
const filesystem = this;
|
|
let args = ["-p", "-o", "export", device];
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("blkid", args);
|
|
} catch (err) {
|
|
if (err.code == 2) {
|
|
return false;
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* blkid -p -o export <device>
|
|
*
|
|
* @param {*} device
|
|
*/
|
|
async getDeviceFilesystemInfo(device) {
|
|
const filesystem = this;
|
|
let args = ["-p", "-o", "export", device];
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("blkid", args);
|
|
const entries = result.stdout.trim().split("\n");
|
|
const properties = {};
|
|
let fields, key, value;
|
|
entries.forEach(entry => {
|
|
fields = entry.split("=");
|
|
key = fields[0].toLowerCase();
|
|
value = fields[1];
|
|
properties[key] = value;
|
|
});
|
|
|
|
return properties;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* mkfs.<fstype> [<options>] device
|
|
*
|
|
* @param {*} device
|
|
* @param {*} fstype
|
|
* @param {*} options
|
|
*/
|
|
async formatDevice(device, fstype, options = []) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
switch (fstype) {
|
|
case "vfat":
|
|
args = args.concat(["-I"]);
|
|
break;
|
|
}
|
|
args.push(device);
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("mkfs." + fstype, args);
|
|
return result;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async realpath(path) {
|
|
const filesystem = this;
|
|
let args = [path];
|
|
let result;
|
|
|
|
try {
|
|
result = await filesystem.exec("realpath", args);
|
|
return result.stdout.trim();
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
async rescanDevice(device) {
|
|
const filesystem = this;
|
|
let result;
|
|
let device_name;
|
|
|
|
result = await filesystem.isBlockDevice(device);
|
|
if (!result) {
|
|
throw new Error(
|
|
`cannot rescan device ${device} because it is not a block device`
|
|
);
|
|
}
|
|
|
|
result = await filesystem.realpath(device);
|
|
device_name = result.split("/").pop();
|
|
|
|
// echo 1 > /sys/block/sdb/device/rescan
|
|
const sys_file = `/sys/block/${device_name}/device/rescan`;
|
|
fs.writeFileSync(sys_file, "1");
|
|
}
|
|
|
|
/**
|
|
* expand a give filesystem
|
|
*
|
|
* @param {*} device
|
|
* @param {*} fstype
|
|
* @param {*} options
|
|
*/
|
|
async expandFilesystem(device, fstype, options = []) {
|
|
const filesystem = this;
|
|
let command;
|
|
let args = [];
|
|
let result;
|
|
|
|
switch (fstype.toLowerCase()) {
|
|
case "ext4":
|
|
case "ext3":
|
|
case "ext4dev":
|
|
command = "resize2fs";
|
|
args = args.concat(options);
|
|
args.push(device);
|
|
break;
|
|
case "xfs":
|
|
command = "xfs_growfs";
|
|
args = args.concat(options);
|
|
args.push(device); // in this case should be a mounted path
|
|
break;
|
|
case "vfat":
|
|
// must be unmounted
|
|
command = "fatresize";
|
|
args = args.concat(options);
|
|
args = args.concat(["-s", "max"]);
|
|
args.push(device);
|
|
break;
|
|
}
|
|
|
|
try {
|
|
result = await filesystem.exec(command, args);
|
|
return result;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* expand a give filesystem
|
|
*
|
|
* fsck [options] -- [fs-options] [<filesystem> ...]
|
|
*
|
|
* @param {*} device
|
|
* @param {*} fstype
|
|
* @param {*} options
|
|
* @param {*} fsoptions
|
|
*/
|
|
async checkFilesystem(device, fstype, options = [], fsoptions = []) {
|
|
const filesystem = this;
|
|
let command;
|
|
let args = [];
|
|
let result;
|
|
|
|
switch (fstype.toLowerCase()) {
|
|
case "ext4":
|
|
case "ext3":
|
|
case "ext4dev":
|
|
command = "fsck";
|
|
args = args.concat(options);
|
|
args.push(device);
|
|
args.push("--");
|
|
args = args.concat(fsoptions);
|
|
args.push("-f");
|
|
args.push("-p");
|
|
break;
|
|
case "xfs":
|
|
command = "xfs_repair";
|
|
args = args.concat(["-o", "force_geometry"]);
|
|
args = args.concat(options);
|
|
args.push(device);
|
|
break;
|
|
default:
|
|
command = "fsck";
|
|
args = args.concat(options);
|
|
args.push(device);
|
|
args.push("--");
|
|
args = args.concat(fsoptions);
|
|
break;
|
|
}
|
|
|
|
try {
|
|
result = await filesystem.exec(command, args);
|
|
return result;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* mkdir [<options>] <path>
|
|
*
|
|
* @param {*} path
|
|
* @param {*} options
|
|
*/
|
|
async mkdir(path, options = []) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
args.push(path);
|
|
|
|
try {
|
|
await filesystem.exec("mkdir", args);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* rmdir [<options>] <path>
|
|
*
|
|
* @param {*} path
|
|
* @param {*} options
|
|
*/
|
|
async rmdir(path, options = []) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args = args.concat(options);
|
|
args.push(path);
|
|
|
|
try {
|
|
await filesystem.exec("rmdir", args);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {*} path
|
|
*/
|
|
async pathExists(path) {
|
|
const filesystem = this;
|
|
let args = [];
|
|
args.push(path);
|
|
|
|
try {
|
|
await filesystem.exec("stat", args);
|
|
} catch (err) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
exec(command, args, options) {
|
|
const filesystem = this;
|
|
args = args || [];
|
|
|
|
let timeout;
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
if (filesystem.options.sudo) {
|
|
args.unshift(command);
|
|
command = filesystem.options.paths.sudo;
|
|
}
|
|
console.log("executing fileystem command: %s %s", command, args.join(" "));
|
|
const child = filesystem.options.executor.spawn(command, args, options);
|
|
|
|
let didTimeout = false;
|
|
if (options && options.timeout) {
|
|
timeout = setTimeout(() => {
|
|
didTimeout = true;
|
|
child.kill(options.killSignal || "SIGTERM");
|
|
}, options.timeout);
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
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 };
|
|
if (timeout) {
|
|
clearTimeout(timeout);
|
|
}
|
|
if (code) {
|
|
console.log(
|
|
"failed to execute filesystem command: %s, response: %j",
|
|
[command].concat(args).join(" "),
|
|
result
|
|
);
|
|
reject(result);
|
|
} else {
|
|
resolve(result);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
}
|
|
|
|
module.exports.Filesystem = Filesystem;
|