forked from Danuphon/toolkit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathio-util.ts
More file actions
215 lines (190 loc) · 5.12 KB
/
io-util.ts
File metadata and controls
215 lines (190 loc) · 5.12 KB
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
import {ok} from 'assert'
import * as fs from 'fs'
import * as path from 'path'
export const {
chmod,
copyFile,
lstat,
mkdir,
readdir,
readlink,
rename,
rmdir,
stat,
symlink,
unlink
} = fs.promises
export const IS_WINDOWS = process.platform === 'win32'
export async function exists(fsPath: string): Promise<boolean> {
try {
await stat(fsPath)
} catch (err) {
if (err.code === 'ENOENT') {
return false
}
throw err
}
return true
}
export async function isDirectory(
fsPath: string,
useStat: boolean = false
): Promise<boolean> {
const stats = useStat ? await stat(fsPath) : await lstat(fsPath)
return stats.isDirectory()
}
/**
* On OSX/Linux, true if path starts with '/'. On Windows, true for paths like:
* \, \hello, \\hello\share, C:, and C:\hello (and corresponding alternate separator cases).
*/
export function isRooted(p: string): boolean {
p = normalizeSeparators(p)
if (!p) {
throw new Error('isRooted() parameter "p" cannot be empty')
}
if (IS_WINDOWS) {
return (
p.startsWith('\\') || /^[A-Z]:/i.test(p) // e.g. \ or \hello or \\hello
) // e.g. C: or C:\hello
}
return p.startsWith('/')
}
/**
* Recursively create a directory at `fsPath`.
*
* This implementation is optimistic, meaning it attempts to create the full
* path first, and backs up the path stack from there.
*
* @param fsPath The path to create
* @param maxDepth The maximum recursion depth
* @param depth The current recursion depth
*/
export async function mkdirP(
fsPath: string,
maxDepth: number = 1000,
depth: number = 1
): Promise<void> {
ok(fsPath, 'a path argument must be provided')
fsPath = path.resolve(fsPath)
if (depth >= maxDepth) return mkdir(fsPath)
try {
await mkdir(fsPath)
return
} catch (err) {
switch (err.code) {
case 'ENOENT': {
await mkdirP(path.dirname(fsPath), maxDepth, depth + 1)
await mkdir(fsPath)
return
}
default: {
let stats: fs.Stats
try {
stats = await stat(fsPath)
} catch (err2) {
throw err
}
if (!stats.isDirectory()) throw err
}
}
}
}
/**
* Best effort attempt to determine whether a file exists and is executable.
* @param filePath file path to check
* @param extensions additional file extensions to try
* @return if file exists and is executable, returns the file path. otherwise empty string.
*/
export async function tryGetExecutablePath(
filePath: string,
extensions: string[]
): Promise<string> {
let stats: fs.Stats | undefined = undefined
try {
// test file exists
stats = await stat(filePath)
} catch (err) {
if (err.code !== 'ENOENT') {
// eslint-disable-next-line no-console
console.log(
`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`
)
}
}
if (stats && stats.isFile()) {
if (IS_WINDOWS) {
// on Windows, test for valid extension
const upperExt = path.extname(filePath).toUpperCase()
if (extensions.some(validExt => validExt.toUpperCase() === upperExt)) {
return filePath
}
} else {
if (isUnixExecutable(stats)) {
return filePath
}
}
}
// try each extension
const originalFilePath = filePath
for (const extension of extensions) {
filePath = originalFilePath + extension
stats = undefined
try {
stats = await stat(filePath)
} catch (err) {
if (err.code !== 'ENOENT') {
// eslint-disable-next-line no-console
console.log(
`Unexpected error attempting to determine if executable file exists '${filePath}': ${err}`
)
}
}
if (stats && stats.isFile()) {
if (IS_WINDOWS) {
// preserve the case of the actual file (since an extension was appended)
try {
const directory = path.dirname(filePath)
const upperName = path.basename(filePath).toUpperCase()
for (const actualName of await readdir(directory)) {
if (upperName === actualName.toUpperCase()) {
filePath = path.join(directory, actualName)
break
}
}
} catch (err) {
// eslint-disable-next-line no-console
console.log(
`Unexpected error attempting to determine the actual case of the file '${filePath}': ${err}`
)
}
return filePath
} else {
if (isUnixExecutable(stats)) {
return filePath
}
}
}
}
return ''
}
function normalizeSeparators(p: string): string {
p = p || ''
if (IS_WINDOWS) {
// convert slashes on Windows
p = p.replace(/\//g, '\\')
// remove redundant slashes
return p.replace(/\\\\+/g, '\\')
}
// remove redundant slashes
return p.replace(/\/\/+/g, '/')
}
// on Mac/Linux, test the execute bit
// R W X R W X R W X
// 256 128 64 32 16 8 4 2 1
function isUnixExecutable(stats: fs.Stats): boolean {
return (
(stats.mode & 1) > 0 ||
((stats.mode & 8) > 0 && stats.gid === process.getgid()) ||
((stats.mode & 64) > 0 && stats.uid === process.getuid())
)
}