@zowe/imperative
Version:
framework for building configurable CLIs
479 lines • 17.7 kB
JavaScript
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.IO = void 0;
const fs = require("fs");
const path = require("path");
const os = require("os");
const error_1 = require("../../error");
const expect_1 = require("../../expect");
// use complete path to ExecUtils to avoid circular dependency that results from utilities/index
const ExecUtils_1 = require("../../utilities/src/ExecUtils");
/**
* This class will handle common sequences of node I/O and issue messages /
* throw errors as neccessary.
* @export
* @class IO
*/
class IO {
/**
* Return whether input file is a directory or file
* @static
* @param {string} dirOrFile - file path
* @returns {boolean} - true if file path is a directory, false otherwise
* @memberof IO
*/
static isDir(dirOrFile) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(dirOrFile, "dirOrFile");
const stat = fs.statSync(dirOrFile);
return stat.isDirectory();
}
/**
* Take an extension and prefix with a '.' identifier
* @static
* @param {string} extension - extension to normalize
* @returns {string} - '.bin' for input 'bin' for example
* @memberof IO
*/
static normalizeExtension(extension) {
expect_1.ImperativeExpect.toNotBeNullOrUndefined(extension, "extension");
extension = extension.trim();
if (extension != null && extension.length > 0 && extension[0] !== ".") {
// add a '.' character to the extension if omitted
// (if someone specifies just "bin", make the extension ".bin" )
extension = "." + extension;
}
return extension;
}
/**
* Wraps fs.existsSync so that we dont have to import fs unnecessarily
* @static
* @param {string} file - file to validate existence against
* @returns true if file exists
* @memberof IO
*/
static existsSync(file) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
return fs.existsSync(file);
}
/**
* Create a directory and all subdirectories if they do not yet exist synchronously.
* @static
* @param {string} dir - directory to create
* @return {undefined}
* @memberof IO
*/
static createDirSync(dir) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(dir, "dir");
fs.mkdirSync(dir, { recursive: true });
}
/**
* Create all needed directories for an input directory in the form of:
* first/second/third where first will contain director second and second
* will contain directory third
* @static
* @param {string} dir - directory to create all sub directories for
* @memberof IO
* @deprecated Please use IO.createDirSync
*/
static createDirsSync(dir) {
IO.createDirSync(dir);
}
/**
* Create all necessary directories for a fully qualified file and its path,
* for example, if filePath = oneDir/twoDir/threeDir/file.txt,
* oneDir, twoDir, and threeDir will be created.
* @static
* @param {string} filePath [description]
* @return {[type]} [description]
* @memberof IO
*/
static createDirsSyncFromFilePath(filePath) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(filePath, "filePath");
IO.createDirSync(path.dirname(filePath));
}
/**
* Create a symbolic link to a directory. If the symbolic link already exists,
* re-create it with the specified target directory.
*
* @param {string} newSymLinkPath - the path new symbolic link to be created
* @param {string} existingDirPath - the path the existing directory that we will link to
*/
static createSymlinkToDir(newSymLinkPath, existingDirPath) {
try {
if (!fs.existsSync(newSymLinkPath)) {
fs.symlinkSync(existingDirPath, newSymLinkPath, "junction");
return;
}
// Get the file status of the existing intended symlink to ensure it is a symlink.
const fileStats = fs.lstatSync(newSymLinkPath);
if (fileStats.isSymbolicLink()) {
fs.unlinkSync(newSymLinkPath);
fs.symlinkSync(existingDirPath, newSymLinkPath, "junction");
return;
}
}
catch (exception) {
throw new error_1.ImperativeError({
msg: "Failed to create symbolic link from '" + newSymLinkPath +
"' to '" + existingDirPath + "'\n" +
"Reason: " + exception.message + "\n" +
"Full exception: " + exception
});
}
throw new error_1.ImperativeError({
msg: "The intended symlink '" + newSymLinkPath +
"' already exists and is not a symbolic link. So, we did not create a symlink from there to '" +
existingDirPath + "'."
});
}
/**
* Create a directory and all subdirectories if they do not yet exist synchronously.
* @static
* @param {string} dir - the directory (do not include a file name)
* @memberof IO
* @deprecated Please use IO.createDirSync
*/
static mkdirp(dir) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(dir, "dir");
IO.createDirSync(dir);
}
/**
* Wraps fs.readFileSync so that we dont have to import fs unnecessarily
* or specify encoding.
* @static
* @param {string} file - file to read
* @param normalizeNewLines - remove Windows line endings (\r\n) in favor of \n
* @param binary - should the file be read in binary mode? If so, normalizeNewLines is ignored. If false,
* the file will be read in UTF-8 encoding
* @return Buffer - the content of the file
* @memberof IO
*/
static readFileSync(file, normalizeNewLines = false, binary = false) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
if (binary) {
return fs.readFileSync(file);
}
else {
let content = fs.readFileSync(file, IO.UTF8).toString();
if (normalizeNewLines) {
content = content.replace(/\r\n/g, "\n");
}
return Buffer.from(content, IO.UTF8);
}
}
/**
* Create a Node.js Readable stream from a file
* @param file - the file from which to create a read stream
* @return Buffer - the content of the file
* @memberof IO
*/
static createReadStream(file) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
return fs.createReadStream(file, { autoClose: true });
}
/**
* Create a Node.js Readable stream from a file
* @param file - the file from which to create a read stream
* @return Buffer - the content of the file
* @memberof IO
*/
static createWriteStream(file) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
return fs.createWriteStream(file, { autoClose: true });
}
/**
* Process a string so that its line endings are operating system
* appropriate before you save it to disk
* (basically, if the user is on Windows, change \n to \r\n)
* @static
* @param {string} original - original input
* @param {number} lastByte - last byte of previous input, if it is being processed in chunks
* @returns {string} - input with removed newlines
* @memberof IO
*/
static processNewlines(original, lastByte) {
expect_1.ImperativeExpect.toNotBeNullOrUndefined(original, "Required parameter 'original' must not be null or undefined");
if (os.platform() !== IO.OS_WIN32) {
return original;
}
let processed;
if (typeof original === "string") {
processed = original.replace(/([^\r])\n/g, "$1\r\n");
if ((lastByte == null || lastByte !== "\r".charCodeAt(0)) && original.startsWith("\n")) {
return ("\r" + processed);
}
return processed;
}
else {
const bufferList = [];
let prevByte = lastByte;
for (let i = 0; i < original.length; i++) {
const currentByte = original[i];
// Check if previous byte is not Carriage Return (13) and if current byte is Line Feed (10)
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
if (currentByte === 10 && prevByte !== 13) {
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
bufferList.push(13);
}
bufferList.push(currentByte);
prevByte = currentByte;
}
processed = Buffer.from(bufferList);
return processed;
}
}
/**
* Get default text editor for a given operating system
* @static
* @returns {string} - text editor launch string
* @memberof IO
*/
static getDefaultTextEditor() {
const platform = os.platform();
if (platform === IO.OS_WIN32) {
return "notepad";
}
else if (platform === IO.OS_MAC) {
return "open -a TextEdit";
}
else if (platform === IO.OS_LINUX) {
return "vi";
}
}
/**
* Create a file
* @static
* @param {string} file - file to create
* @memberof IO
*/
static createFileSync(file) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
fs.closeSync(fs.openSync(file, "w"));
}
/**
* Set file access permissions so that only the current user will have access to the file.
* On windows, it means "full control" for the current user.
* On posix, it means read & write access for the current user.
* Obviously, the current user must have permission to change permissions
* on the specified file.
*
* @param {string} fileName - file name for which we modify access.
* @throws An ImperativeError when the operation fails.
*/
static giveAccessOnlyToOwner(fileName) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(fileName, "fileName");
try {
if (!IO.existsSync(fileName)) {
throw new Error("Attempted to restrict access on a non-existent file.");
}
if (os.platform() === IO.OS_WIN32) {
// On windows, we use an icacls command to prevent access
const stdout = ExecUtils_1.ExecUtils.spawnAndGetOutput("icacls", [fileName, "/inheritancelevel:r", "/grant:r", `${os.userInfo().username}:F`, "/c"], { encoding: "utf8" });
if (!stdout.includes("Successfully processed 1 files; Failed processing 0 files")) {
throw new Error(`icacls reports that it could not change file access:\n${stdout}`);
}
}
else { // we are on Posix
// give read & write permissions to owner
let ownerPrivileges = fs.constants.S_IRUSR | fs.constants.S_IWUSR;
// give execute permission, if owner had it before
if (fs.statSync(fileName).mode & fs.constants.S_IXUSR) {
ownerPrivileges |= fs.constants.S_IXUSR;
}
fs.chmodSync(fileName, ownerPrivileges);
}
}
catch (errObj) {
throw new error_1.ImperativeError({
msg: `Failed to restrict access to others on file = ${fileName}\nError Details: ${errObj.message}`
});
}
}
/**
* Create a file asynchronously
* @static
* @param {string} file - file to create
* @param {string} content - content to write in the file
* @return {[type]} [description]
* @memberof IO
*/
static writeFileAsync(file, content) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
expect_1.ImperativeExpect.toNotBeNullOrUndefined(content, "Content to write to the file must not be null or undefined");
return new Promise((resolve, reject) => {
fs.writeFile(file, content, IO.UTF8, (err) => {
if (err != null) {
reject(new error_1.ImperativeError({ msg: err.message }));
}
resolve();
});
});
}
/**
* Write a file
* @static
* @param {string} file - file to create
* @param {string} content - content to write
* @return {undefined}
* @memberof IO
*/
static writeFile(file, content) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
expect_1.ImperativeExpect.toNotBeNullOrUndefined(content, "Content to write to the file must not be null or undefined");
const fd = fs.openSync(file, "w");
try {
fs.writeFileSync(fd, content);
}
finally {
fs.closeSync(fd);
}
}
/**
* Write an object to a file and set consistent formatting on the serialized
* JSON object.
* @static
* @param {string} configFile - file to create
* @param {Object} object - object to serialize
* @return {undefined}
* @memberof IO
*/
static writeObject(configFile, object) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(configFile, "configFile");
expect_1.ImperativeExpect.toNotBeNullOrUndefined(object, "content");
const fd = fs.openSync(configFile, "w");
try {
fs.writeFileSync(fd, JSON.stringify(object, null, 2));
}
finally {
fs.closeSync(fd);
}
}
/**
* Delete a file
* @static
* @param {string} file: The file to delete
* @memberof IO
*/
static deleteFile(file) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
}
/**
* Delete a directory
* @static
* @param {string} dir: The directory to delete
* @memberof IO
*/
static deleteDir(dir) {
expect_1.ImperativeExpect.toBeDefinedAndNonBlank(dir, "dir");
fs.rmdirSync(dir);
}
/**
* Recursively delete all files and subdirectories of the specified directory.
* Ensure that we do not follow a symlink. Just delete the link.
*
* @params {string} pathToTreeToDelete - Path to top directory of the tree
* to delete.
*/
static deleteDirTree(pathToTreeToDelete) {
try {
// if pathToTreeToDelete is a symlink, just delete the link file
if (fs.existsSync(pathToTreeToDelete)) {
const fileStats = fs.lstatSync(pathToTreeToDelete);
if (fileStats.isSymbolicLink() || fileStats.isFile()) {
fs.unlinkSync(pathToTreeToDelete);
return;
}
// read all of the children of this directory
fs.readdirSync(pathToTreeToDelete).forEach((nextChild) => {
// recursively delete the child
IO.deleteDirTree(pathToTreeToDelete + path.sep + nextChild);
});
// delete our starting directory
fs.rmdirSync(pathToTreeToDelete);
}
}
catch (exception) {
throw new error_1.ImperativeError({
msg: "Failed to delete the directory tree '" + pathToTreeToDelete +
"'\nReason: " + exception.message + "\n" +
"Full exception: " + exception
});
}
}
/**
* Delete a symbolic link.
*
* @param {string} symLinkPath - the path to a symbolic link to be deleted
*/
static deleteSymLink(symLinkPath) {
try {
if (!fs.existsSync(symLinkPath)) {
return;
}
// Get the file status to determine if it is a symlink.
const fileStats = fs.lstatSync(symLinkPath);
if (fileStats.isSymbolicLink()) {
fs.unlinkSync(symLinkPath);
return;
}
}
catch (ioExcept) {
throw new error_1.ImperativeError({
msg: "Failed to delete the symbolic link '" + symLinkPath +
"'\nReason: " + ioExcept.message + "\n" +
"Full exception: " + ioExcept
});
}
throw new error_1.ImperativeError({
msg: "The specified symlink '" + symLinkPath +
"' already exists and is not a symbolic link. So, we did not delete it."
});
}
}
exports.IO = IO;
/**
* File delimiter
* @deprecated Use `path.posix.sep` instead or `path.sep` for better cross-platform support
* @static
* @type {string}
* @memberof IO
*/
IO.FILE_DELIM = path.posix.sep;
/**
* UTF8 identifier
* @static
* @memberof IO
*/
IO.UTF8 = "utf8";
/**
* Windows OS identifier
* @static
* @memberof IO
*/
IO.OS_WIN32 = "win32";
/**
* Mac OS identifier
* @static
* @memberof IO
*/
IO.OS_MAC = "darwin";
/**
* Linux OS identifier
* @static
* @memberof IO
*/
IO.OS_LINUX = "linux";
//# sourceMappingURL=IO.js.map
;