-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
io.ts
327 lines (288 loc) · 8.98 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
import {ok} from 'assert'
import * as path from 'path'
import * as ioUtil from './io-util'
/**
* Interface for cp/mv options
*/
export interface CopyOptions {
/** Optional. Whether to recursively copy all subdirectories. Defaults to false */
recursive?: boolean
/** Optional. Whether to overwrite existing files in the destination. Defaults to true */
force?: boolean
/** Optional. Whether to copy the source directory along with all the files. Only takes effect when recursive=true and copying a directory. Default is true*/
copySourceDirectory?: boolean
}
/**
* Interface for cp/mv options
*/
export interface MoveOptions {
/** Optional. Whether to overwrite existing files in the destination. Defaults to true */
force?: boolean
}
/**
* Copies a file or folder.
* Based off of shelljs - https://github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js
*
* @param source source path
* @param dest destination path
* @param options optional. See CopyOptions.
*/
export async function cp(
source: string,
dest: string,
options: CopyOptions = {}
): Promise<void> {
const {force, recursive, copySourceDirectory} = readCopyOptions(options)
const destStat = (await ioUtil.exists(dest)) ? await ioUtil.stat(dest) : null
// Dest is an existing file, but not forcing
if (destStat && destStat.isFile() && !force) {
return
}
// If dest is an existing directory, should copy inside.
const newDest: string =
destStat && destStat.isDirectory() && copySourceDirectory
? path.join(dest, path.basename(source))
: dest
if (!(await ioUtil.exists(source))) {
throw new Error(`no such file or directory: ${source}`)
}
const sourceStat = await ioUtil.stat(source)
if (sourceStat.isDirectory()) {
if (!recursive) {
throw new Error(
`Failed to copy. ${source} is a directory, but tried to copy without recursive flag.`
)
} else {
await cpDirRecursive(source, newDest, 0, force)
}
} else {
if (path.relative(source, newDest) === '') {
// a file cannot be copied to itself
throw new Error(`'${newDest}' and '${source}' are the same file`)
}
await copyFile(source, newDest, force)
}
}
/**
* Moves a path.
*
* @param source source path
* @param dest destination path
* @param options optional. See MoveOptions.
*/
export async function mv(
source: string,
dest: string,
options: MoveOptions = {}
): Promise<void> {
if (await ioUtil.exists(dest)) {
let destExists = true
if (await ioUtil.isDirectory(dest)) {
// If dest is directory copy src into dest
dest = path.join(dest, path.basename(source))
destExists = await ioUtil.exists(dest)
}
if (destExists) {
if (options.force == null || options.force) {
await rmRF(dest)
} else {
throw new Error('Destination already exists')
}
}
}
await mkdirP(path.dirname(dest))
await ioUtil.rename(source, dest)
}
/**
* Remove a path recursively with force
*
* @param inputPath path to remove
*/
export async function rmRF(inputPath: string): Promise<void> {
if (ioUtil.IS_WINDOWS) {
// Check for invalid characters
// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file
if (/[*"<>|]/.test(inputPath)) {
throw new Error(
'File path must not contain `*`, `"`, `<`, `>` or `|` on Windows'
)
}
}
try {
// note if path does not exist, error is silent
await ioUtil.rm(inputPath, {
force: true,
maxRetries: 3,
recursive: true,
retryDelay: 300
})
} catch (err) {
throw new Error(`File was unable to be removed ${err}`)
}
}
/**
* Make a directory. Creates the full path with folders in between
* Will throw if it fails
*
* @param fsPath path to create
* @returns Promise<void>
*/
export async function mkdirP(fsPath: string): Promise<void> {
ok(fsPath, 'a path argument must be provided')
await ioUtil.mkdir(fsPath, {recursive: true})
}
/**
* Returns path of a tool had the tool actually been invoked. Resolves via paths.
* If you check and the tool does not exist, it will throw.
*
* @param tool name of the tool
* @param check whether to check if tool exists
* @returns Promise<string> path to tool
*/
export async function which(tool: string, check?: boolean): Promise<string> {
if (!tool) {
throw new Error("parameter 'tool' is required")
}
// recursive when check=true
if (check) {
const result: string = await which(tool, false)
if (!result) {
if (ioUtil.IS_WINDOWS) {
throw new Error(
`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also verify the file has a valid extension for an executable file.`
)
} else {
throw new Error(
`Unable to locate executable file: ${tool}. Please verify either the file path exists or the file can be found within a directory specified by the PATH environment variable. Also check the file mode to verify the file is executable.`
)
}
}
return result
}
const matches: string[] = await findInPath(tool)
if (matches && matches.length > 0) {
return matches[0]
}
return ''
}
/**
* Returns a list of all occurrences of the given tool on the system path.
*
* @returns Promise<string[]> the paths of the tool
*/
export async function findInPath(tool: string): Promise<string[]> {
if (!tool) {
throw new Error("parameter 'tool' is required")
}
// build the list of extensions to try
const extensions: string[] = []
if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) {
for (const extension of process.env['PATHEXT'].split(path.delimiter)) {
if (extension) {
extensions.push(extension)
}
}
}
// if it's rooted, return it if exists. otherwise return empty.
if (ioUtil.isRooted(tool)) {
const filePath: string = await ioUtil.tryGetExecutablePath(tool, extensions)
if (filePath) {
return [filePath]
}
return []
}
// if any path separators, return empty
if (tool.includes(path.sep)) {
return []
}
// build the list of directories
//
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
// it feels like we should not do this. Checking the current directory seems like more of a use
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
// across platforms.
const directories: string[] = []
if (process.env.PATH) {
for (const p of process.env.PATH.split(path.delimiter)) {
if (p) {
directories.push(p)
}
}
}
// find all matches
const matches: string[] = []
for (const directory of directories) {
const filePath = await ioUtil.tryGetExecutablePath(
path.join(directory, tool),
extensions
)
if (filePath) {
matches.push(filePath)
}
}
return matches
}
function readCopyOptions(options: CopyOptions): Required<CopyOptions> {
const force = options.force == null ? true : options.force
const recursive = Boolean(options.recursive)
const copySourceDirectory =
options.copySourceDirectory == null
? true
: Boolean(options.copySourceDirectory)
return {force, recursive, copySourceDirectory}
}
async function cpDirRecursive(
sourceDir: string,
destDir: string,
currentDepth: number,
force: boolean
): Promise<void> {
// Ensure there is not a run away recursive copy
if (currentDepth >= 255) return
currentDepth++
await mkdirP(destDir)
const files: string[] = await ioUtil.readdir(sourceDir)
for (const fileName of files) {
const srcFile = `${sourceDir}/${fileName}`
const destFile = `${destDir}/${fileName}`
const srcFileStat = await ioUtil.lstat(srcFile)
if (srcFileStat.isDirectory()) {
// Recurse
await cpDirRecursive(srcFile, destFile, currentDepth, force)
} else {
await copyFile(srcFile, destFile, force)
}
}
// Change the mode for the newly created directory
await ioUtil.chmod(destDir, (await ioUtil.stat(sourceDir)).mode)
}
// Buffered file copy
async function copyFile(
srcFile: string,
destFile: string,
force: boolean
): Promise<void> {
if ((await ioUtil.lstat(srcFile)).isSymbolicLink()) {
// unlink/re-link it
try {
await ioUtil.lstat(destFile)
await ioUtil.unlink(destFile)
} catch (e) {
// Try to override file permission
if (e.code === 'EPERM') {
await ioUtil.chmod(destFile, '0666')
await ioUtil.unlink(destFile)
}
// other errors = it doesn't exist, no work to do
}
// Copy over symlink
const symlinkFull: string = await ioUtil.readlink(srcFile)
await ioUtil.symlink(
symlinkFull,
destFile,
ioUtil.IS_WINDOWS ? 'junction' : null
)
} else if (!(await ioUtil.exists(destFile)) || force) {
await ioUtil.copyFile(srcFile, destFile)
}
}