mirror of
https://git.mirrors.martin98.com/https://github.com/actions/toolkit
synced 2026-04-05 03:33:15 +08:00
Add io (#5)
* Add io lib * io cleanup * Run format script * Fix lint errors with autofix * Fix equality lint errors * Rename ioUtil to io-util * Add no-import-requires * Run auto-fix lint * Remove lint errors * Use Boolean() to convert options - `CopyOptions` on `cp` now defaults to empty - Setting option values is easier now * Rewrite packages/io to be fully async * Move IS_WINDOWS into ioUtil * DRY up cp/mv by moving shared code into move function * Remove unc support, change isDirectory call to stat * Tighter try catches * more concise extensions search * Allow isDirectory to be stat or lstat * format * Shell out to rm -rf * Remove unc comment * Export fs.promises from io-util * Remove unknown error message * Create an optimistic mkdirp * Update io-util.ts * Update io-util.ts * Update io.test.ts * Fix tests for mkdirP
This commit is contained in:
211
packages/io/src/io-util.ts
Normal file
211
packages/io/src/io-util.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import {ok} from 'assert'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
|
||||
export const {
|
||||
copyFile,
|
||||
lstat,
|
||||
mkdir,
|
||||
readdir,
|
||||
rmdir,
|
||||
stat,
|
||||
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())
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user