mirror of
https://git.mirrors.martin98.com/https://github.com/actions/toolkit
synced 2026-04-01 23:23:16 +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:
286
packages/io/src/io.ts
Normal file
286
packages/io/src/io.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
import * as childProcess from 'child_process'
|
||||
import * as fs from 'fs'
|
||||
import * as path from 'path'
|
||||
import {promisify} from 'util'
|
||||
import * as ioUtil from './io-util'
|
||||
|
||||
const exec = promisify(childProcess.exec)
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies a file or folder.
|
||||
*
|
||||
* @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> {
|
||||
await move(source, dest, options, {deleteOriginal: false})
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a path.
|
||||
*
|
||||
* @param source source path
|
||||
* @param dest destination path
|
||||
* @param options optional. See CopyOptions.
|
||||
*/
|
||||
export async function mv(
|
||||
source: string,
|
||||
dest: string,
|
||||
options: CopyOptions = {}
|
||||
): Promise<void> {
|
||||
await move(source, dest, options, {deleteOriginal: true})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a path recursively with force
|
||||
*
|
||||
* @param inputPath path to remove
|
||||
*/
|
||||
export async function rmRF(inputPath: string): Promise<void> {
|
||||
if (ioUtil.IS_WINDOWS) {
|
||||
// Node doesn't provide a delete operation, only an unlink function. This means that if the file is being used by another
|
||||
// program (e.g. antivirus), it won't be deleted. To address this, we shell out the work to rd/del.
|
||||
try {
|
||||
if (await ioUtil.isDirectory(inputPath, true)) {
|
||||
await exec(`rd /s /q "${inputPath}"`)
|
||||
} else {
|
||||
await exec(`del /f /a "${inputPath}"`)
|
||||
}
|
||||
} catch (err) {
|
||||
// if you try to delete a file that doesn't exist, desired result is achieved
|
||||
// other errors are valid
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
}
|
||||
|
||||
// Shelling out fails to remove a symlink folder with missing source, this unlink catches that
|
||||
try {
|
||||
await ioUtil.unlink(inputPath)
|
||||
} catch (err) {
|
||||
// if you try to delete a file that doesn't exist, desired result is achieved
|
||||
// other errors are valid
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
}
|
||||
} else {
|
||||
let isDir = false
|
||||
try {
|
||||
isDir = await ioUtil.isDirectory(inputPath)
|
||||
} catch (err) {
|
||||
// if you try to delete a file that doesn't exist, desired result is achieved
|
||||
// other errors are valid
|
||||
if (err.code !== 'ENOENT') throw err
|
||||
return
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
await exec(`rm -rf "${inputPath}"`)
|
||||
} else {
|
||||
await ioUtil.unlink(inputPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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> {
|
||||
await ioUtil.mkdirP(fsPath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 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('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// build the list of directories
|
||||
//
|
||||
// Note, technically "where" checks the current directory on Windows. From a task lib 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 task lib 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// return the first match
|
||||
for (const directory of directories) {
|
||||
const filePath = await ioUtil.tryGetExecutablePath(
|
||||
directory + path.sep + tool,
|
||||
extensions
|
||||
)
|
||||
if (filePath) {
|
||||
return filePath
|
||||
}
|
||||
}
|
||||
|
||||
return ''
|
||||
} catch (err) {
|
||||
throw new Error(`which failed with message ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Copies contents of source into dest, making any necessary folders along the way.
|
||||
// Deletes the original copy if deleteOriginal is true
|
||||
async function copyDirectoryContents(
|
||||
source: string,
|
||||
dest: string,
|
||||
force: boolean,
|
||||
deleteOriginal = false
|
||||
): Promise<void> {
|
||||
if (await ioUtil.isDirectory(source)) {
|
||||
if (await ioUtil.exists(dest)) {
|
||||
if (!(await ioUtil.isDirectory(dest))) {
|
||||
throw new Error(`${dest} is not a directory`)
|
||||
}
|
||||
} else {
|
||||
await mkdirP(dest)
|
||||
}
|
||||
|
||||
// Copy all child files, and directories recursively
|
||||
const sourceChildren: string[] = await ioUtil.readdir(source)
|
||||
|
||||
for (const newSource of sourceChildren) {
|
||||
const newDest = path.join(dest, path.basename(newSource))
|
||||
await copyDirectoryContents(
|
||||
path.resolve(source, newSource),
|
||||
newDest,
|
||||
force,
|
||||
deleteOriginal
|
||||
)
|
||||
}
|
||||
|
||||
if (deleteOriginal) {
|
||||
await ioUtil.rmdir(source)
|
||||
}
|
||||
} else {
|
||||
if (force) {
|
||||
await ioUtil.copyFile(source, dest)
|
||||
} else {
|
||||
await ioUtil.copyFile(source, dest, fs.constants.COPYFILE_EXCL)
|
||||
}
|
||||
if (deleteOriginal) {
|
||||
await ioUtil.unlink(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function move(
|
||||
source: string,
|
||||
dest: string,
|
||||
options: CopyOptions = {},
|
||||
moveOptions: {deleteOriginal: boolean}
|
||||
): Promise<void> {
|
||||
const {force, recursive} = readCopyOptions(options)
|
||||
|
||||
if (await ioUtil.isDirectory(source)) {
|
||||
if (!recursive) {
|
||||
throw new Error(`non-recursive cp failed, ${source} is a directory`)
|
||||
}
|
||||
|
||||
// If directory exists, move source inside it. Otherwise, create it and move contents of source inside.
|
||||
if (await ioUtil.exists(dest)) {
|
||||
if (!(await ioUtil.isDirectory(dest))) {
|
||||
throw new Error(`${dest} is not a directory`)
|
||||
}
|
||||
|
||||
dest = path.join(dest, path.basename(source))
|
||||
}
|
||||
|
||||
await copyDirectoryContents(source, dest, force, moveOptions.deleteOriginal)
|
||||
} else {
|
||||
if (force) {
|
||||
await ioUtil.copyFile(source, dest)
|
||||
} else {
|
||||
await ioUtil.copyFile(source, dest, fs.constants.COPYFILE_EXCL)
|
||||
}
|
||||
|
||||
if (moveOptions.deleteOriginal) {
|
||||
await ioUtil.unlink(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function readCopyOptions(options: CopyOptions): Required<CopyOptions> {
|
||||
const force = options.force == null ? true : options.force
|
||||
const recursive = Boolean(options.recursive)
|
||||
return {force, recursive}
|
||||
}
|
||||
Reference in New Issue
Block a user