This repository has been archived by the owner on Nov 13, 2023. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 31
/
IO.ts
450 lines (413 loc) · 14.9 KB
/
IO.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
/*
* 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.
*
*/
import * as fs from "fs";
import * as path from "path";
import * as os from "os";
import { isNullOrUndefined } from "util";
import { ImperativeReject } from "../../interfaces";
import { ImperativeError } from "../../error";
import { ImperativeExpect } from "../../expect";
import { Readable, Writable } from "stream";
import { mkdirpSync } from "fs-extra";
/**
* This class will handle common sequences of node I/O and issue messages /
* throw errors as neccessary.
* @export
* @class IO
*/
export class IO {
/**
* File delimiter
* @static
* @type {string}
* @memberof IO
*/
public static readonly FILE_DELIM: string = "/";
/**
* UTF8 identifier
* @static
* @memberof IO
*/
public static readonly UTF8 = "utf8";
/**
* Windows OS identifier
* @static
* @memberof IO
*/
public static readonly OS_WIN32 = "win32";
/**
* Mac OS identifier
* @static
* @memberof IO
*/
public static readonly OS_MAC = "darwin";
/**
* Linux OS identifier
* @static
* @memberof IO
*/
public static readonly OS_LINUX = "linux";
/**
* 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
*/
public static isDir(dirOrFile: string): boolean {
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
*/
public static normalizeExtension(extension: string): string {
ImperativeExpect.toNotBeNullOrUndefined(extension, "extension");
extension = extension.trim();
if (!isNullOrUndefined(extension) && 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
*/
public static existsSync(file: string) {
ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
return fs.existsSync(file);
}
/**
* Create a directory if it does not yet exist synchronously.
* @static
* @param {string} dir - directory to create
* @return {undefined}
* @memberof IO
*/
public static createDirSync(dir: string) {
ImperativeExpect.toBeDefinedAndNonBlank(dir, "dir");
if (!IO.existsSync(dir)) {
fs.mkdirSync(dir);
}
}
/**
* 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
*/
public static createDirsSync(dir: string) {
ImperativeExpect.toBeDefinedAndNonBlank(dir, "dir");
// we're splitting on a specific separator character, so replace \ with /
// before splitting
const dirs = path.resolve(dir).replace(/\\/g, IO.FILE_DELIM).split(IO.FILE_DELIM);
let createDir: string = "";
for (const crDir of dirs) {
createDir += (crDir + IO.FILE_DELIM);
IO.createDirSync(createDir);
}
}
/**
* 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
*/
public static createDirsSyncFromFilePath(filePath: string) {
ImperativeExpect.toBeDefinedAndNonBlank(filePath, "filePath");
IO.createDirsSync(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
*/
public static createSymlinkToDir(newSymLinkPath: string, existingDirPath: string) {
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 ImperativeError({
msg: "Failed to create symbolic link from '" + newSymLinkPath +
"' to '" + existingDirPath + "'\n" +
"Reason: " + exception.message + "\n" +
"Full exception: " + exception
}
);
}
throw new 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 + "'."
}
);
}
/**
* Uses the fs-extra package to create a directory (and all subdirectories)
* @static
* @param {string} dir - the directory (do not include a file name)
* @memberof IO
*/
public static mkdirp(dir: string) {
ImperativeExpect.toBeDefinedAndNonBlank(dir, "dir");
mkdirpSync(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
*/
public static readFileSync(file: string, normalizeNewLines: boolean = false, binary: boolean = false): Buffer {
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
*/
public static createReadStream(file: string): Readable {
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
*/
public static createWriteStream(file: string): Writable {
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
* @returns {string} - input with removed newlines
* @memberof IO
*/
public static processNewlines(original: string): string {
ImperativeExpect.toNotBeNullOrUndefined(original, "Required parameter 'original' must not be null or undefined");
if (os.platform() !== IO.OS_WIN32) {
return original;
}
// otherwise, we're on windows
return original.replace(/([^\r])\n/g, "$1\r\n");
}
/**
* Get default text editor for a given operating system
* @static
* @returns {string} - text editor launch string
* @memberof IO
*/
public static getDefaultTextEditor(): string {
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 "gedit";
}
}
/**
* Create a file
* @static
* @param {string} file - file to create
* @memberof IO
*/
public static createFileSync(file: string) {
ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
fs.closeSync(fs.openSync(file, "w"));
}
/**
* 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
*/
public static writeFileAsync(file: string, content: string) {
ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
ImperativeExpect.toNotBeNullOrUndefined(content, "Content to write to the file must not be null or undefined");
return new Promise<void>((resolve, reject: ImperativeReject) => {
try {
fs.writeFile(file, content, IO.UTF8, (err) => {
if (!isNullOrUndefined(err)) {
throw new ImperativeError({msg: err.message});
}
resolve();
});
} catch (error) {
throw new ImperativeError({msg: error.message});
}
});
}
/**
* Write a file
* @static
* @param {string} file - file to create
* @param {string} content - content to write
* @return {undefined}
* @memberof IO
*/
public static writeFile(file: string, content: Buffer) {
ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
ImperativeExpect.toNotBeNullOrUndefined(content, "Content to write to the file must not be null or undefined");
IO.createFileSync(file);
fs.writeFileSync(file, content);
}
/**
* 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
*/
public static writeObject(configFile: string, object: object) {
ImperativeExpect.toBeDefinedAndNonBlank(configFile, "configFile");
ImperativeExpect.toNotBeNullOrUndefined(object, "content");
fs.closeSync(fs.openSync(configFile, "w"));
fs.appendFileSync(configFile, JSON.stringify(object, null, 2));
}
/**
* Delete a file
* @static
* @param {string} file: The file to delete
* @memberof IO
*/
public static deleteFile(file: string) {
ImperativeExpect.toBeDefinedAndNonBlank(file, "file");
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
}
/**
* Delete a directory
* @static
* @param {string} dir: The directory to delete
* @memberof IO
*/
public static deleteDir(dir: string) {
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.
*/
public static deleteDirTree(pathToTreeToDelete: string) {
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, index) => {
// recursively delete the child
IO.deleteDirTree(pathToTreeToDelete + path.sep + nextChild);
});
// delete our starting directory
fs.rmdirSync(pathToTreeToDelete);
}
} catch (exception) {
throw new 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
*/
public static deleteSymLink(symLinkPath: string) {
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 ImperativeError({
msg: "Failed to delete the symbolic link '" + symLinkPath +
"'\nReason: " + ioExcept.message + "\n" +
"Full exception: " + ioExcept
}
);
}
throw new ImperativeError({
msg: "The specified symlink '" + symLinkPath +
"' already exists and is not a symbolic link. So, we did not delete it."
}
);
}
}