mirror of https://github.com/actions/toolkit
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 mkdirPpull/11/head
parent
4f5f4f2fb8
commit
08db5110c6
|
@ -26,8 +26,6 @@
|
||||||
"@typescript-eslint/no-array-constructor": "error",
|
"@typescript-eslint/no-array-constructor": "error",
|
||||||
"@typescript-eslint/no-empty-interface": "error",
|
"@typescript-eslint/no-empty-interface": "error",
|
||||||
"@typescript-eslint/no-explicit-any": "error",
|
"@typescript-eslint/no-explicit-any": "error",
|
||||||
"no-extra-parens": "off",
|
|
||||||
"@typescript-eslint/no-extra-parens": "error",
|
|
||||||
"@typescript-eslint/no-extraneous-class": "error",
|
"@typescript-eslint/no-extraneous-class": "error",
|
||||||
"@typescript-eslint/no-for-in-array": "error",
|
"@typescript-eslint/no-for-in-array": "error",
|
||||||
"@typescript-eslint/no-inferrable-types": "error",
|
"@typescript-eslint/no-inferrable-types": "error",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
packages/*/node_modules/
|
packages/*/node_modules/
|
||||||
packages/*/lib/
|
packages/*/lib/
|
||||||
|
packages/*/__tests__/_temp/
|
|
@ -0,0 +1,49 @@
|
||||||
|
# `@actions/io`
|
||||||
|
|
||||||
|
> Core functions for cli filesystem scenarios
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/**
|
||||||
|
* Copies a file or folder.
|
||||||
|
*
|
||||||
|
* @param source source path
|
||||||
|
* @param dest destination path
|
||||||
|
* @param options optional. See CopyOptions.
|
||||||
|
*/
|
||||||
|
export function cp(source: string, dest: string, options?: CopyOptions): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a path recursively with force
|
||||||
|
*
|
||||||
|
* @param path path to remove
|
||||||
|
*/
|
||||||
|
export function rmRF(path: string): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a directory. Creates the full path with folders in between
|
||||||
|
*
|
||||||
|
* @param p path to create
|
||||||
|
* @returns Promise<void>
|
||||||
|
*/
|
||||||
|
export function mkdirP(p: string): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Moves a path.
|
||||||
|
*
|
||||||
|
* @param source source path
|
||||||
|
* @param dest destination path
|
||||||
|
* @param options optional. See CopyOptions.
|
||||||
|
*/
|
||||||
|
export function mv(source: string, dest: string, options?: CopyOptions): Promise<void>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns path of a tool had the tool actually been invoked. Resolves via paths.
|
||||||
|
*
|
||||||
|
* @param tool name of the tool
|
||||||
|
* @param options optional. See WhichOptions.
|
||||||
|
* @returns Promise<string> path to tool
|
||||||
|
*/
|
||||||
|
export function which(tool: string, options?: WhichOptions): Promise<string>
|
||||||
|
```
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"name": "@actions/io",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Actions io lib",
|
||||||
|
"keywords": [
|
||||||
|
"io",
|
||||||
|
"actions"
|
||||||
|
],
|
||||||
|
"author": "Danny McCormick <damccorm@microsoft.com>",
|
||||||
|
"homepage": "https://github.com/actions/toolkit/tree/master/packages/io",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "lib/io.js",
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib",
|
||||||
|
"test": "__tests__"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/actions/toolkit.git"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||||
|
"tsc": "tsc"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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())
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue