2021-04-28 18:38:41 +00:00
import { ok } from 'assert'
2019-05-22 20:05:34 +00:00
import * as childProcess from 'child_process'
import * as path from 'path'
import { promisify } from 'util'
import * as ioUtil from './io-util'
const exec = promisify ( childProcess . exec )
2021-06-07 18:16:16 +00:00
const execFile = promisify ( childProcess . execFile )
2019-05-22 20:05:34 +00:00
/ * *
* 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
2021-05-05 13:40:12 +00:00
/** 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
2019-05-22 20:05:34 +00:00
}
2019-07-09 13:56:01 +00:00
/ * *
* Interface for cp / mv options
* /
export interface MoveOptions {
/** Optional. Whether to overwrite existing files in the destination. Defaults to true */
force? : boolean
}
2019-05-22 20:05:34 +00:00
/ * *
* Copies a file or folder .
2019-07-09 13:56:01 +00:00
* Based off of shelljs - https : //github.com/shelljs/shelljs/blob/9237f66c52e5daa40458f94f9565e18e8132f5a6/src/cp.js
2019-05-22 20:05:34 +00:00
*
* @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 > {
2021-05-05 13:40:12 +00:00
const { force , recursive , copySourceDirectory } = readCopyOptions ( options )
2019-07-09 13:56:01 +00:00
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 =
2021-05-05 13:40:12 +00:00
destStat && destStat . isDirectory ( ) && copySourceDirectory
2019-07-09 13:56:01 +00:00
? 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 )
}
2019-05-22 20:05:34 +00:00
}
/ * *
* Moves a path .
*
* @param source source path
* @param dest destination path
2019-07-09 13:56:01 +00:00
* @param options optional . See MoveOptions .
2019-05-22 20:05:34 +00:00
* /
export async function mv (
source : string ,
dest : string ,
2019-07-09 13:56:01 +00:00
options : MoveOptions = { }
2019-05-22 20:05:34 +00:00
) : Promise < void > {
2019-07-09 13:56:01 +00:00
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 )
2019-05-22 20:05:34 +00:00
}
/ * *
* 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.
2021-06-07 18:16:16 +00:00
// 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'
)
}
2019-05-22 20:05:34 +00:00
try {
2021-06-07 18:16:16 +00:00
const cmdPath = ioUtil . getCmdPath ( )
2019-05-22 20:05:34 +00:00
if ( await ioUtil . isDirectory ( inputPath , true ) ) {
2021-06-07 18:16:16 +00:00
await exec ( ` ${ cmdPath } /s /c "rd /s /q "%inputPath%"" ` , {
env : { inputPath }
} )
2019-05-22 20:05:34 +00:00
} else {
2021-06-07 18:16:16 +00:00
await exec ( ` ${ cmdPath } /s /c "del /f /a "%inputPath%"" ` , {
env : { inputPath }
} )
2019-05-22 20:05:34 +00:00
}
} 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 ) {
2021-06-07 18:16:16 +00:00
await execFile ( ` rm ` , [ ` -rf ` , ` ${ inputPath } ` ] )
2019-05-22 20:05:34 +00:00
} 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 > {
2021-04-28 18:38:41 +00:00
ok ( fsPath , 'a path argument must be provided' )
await ioUtil . mkdir ( fsPath , { recursive : true } )
2019-05-22 20:05:34 +00:00
}
/ * *
* 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. `
)
}
}
2021-04-02 16:22:30 +00:00
return result
2019-05-22 20:05:34 +00:00
}
2021-04-02 16:22:30 +00:00
const matches : string [ ] = await findInPath ( tool )
2019-05-22 20:05:34 +00:00
2021-04-02 16:22:30 +00:00
if ( matches && matches . length > 0 ) {
return matches [ 0 ]
}
2019-05-22 20:05:34 +00:00
2021-04-02 16:22:30 +00:00
return ''
}
2019-05-22 20:05:34 +00:00
2021-04-02 16:22:30 +00:00
/ * *
* 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" )
}
2019-05-22 20:05:34 +00:00
2021-04-02 16:22:30 +00:00
// 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 )
}
2019-05-22 20:05:34 +00:00
}
2021-04-02 16:22:30 +00:00
}
2019-05-22 20:05:34 +00:00
2021-04-02 16:22:30 +00:00
// 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 ]
2019-05-22 20:05:34 +00:00
}
2021-04-02 16:22:30 +00:00
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 )
2019-05-22 20:05:34 +00:00
}
}
2021-04-02 16:22:30 +00:00
}
2019-05-22 20:05:34 +00:00
2021-04-02 16:22:30 +00:00
// 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 )
}
2019-05-22 20:05:34 +00:00
}
2021-04-02 16:22:30 +00:00
return matches
2019-05-22 20:05:34 +00:00
}
2019-07-09 13:56:01 +00:00
function readCopyOptions ( options : CopyOptions ) : Required < CopyOptions > {
const force = options . force == null ? true : options . force
const recursive = Boolean ( options . recursive )
2021-05-05 13:40:12 +00:00
const copySourceDirectory =
options . copySourceDirectory == null
? true
: Boolean ( options . copySourceDirectory )
return { force , recursive , copySourceDirectory }
2019-07-09 13:56:01 +00:00
}
async function cpDirRecursive (
sourceDir : string ,
destDir : string ,
currentDepth : number ,
force : boolean
2019-05-22 20:05:34 +00:00
) : Promise < void > {
2019-07-09 13:56:01 +00:00
// Ensure there is not a run away recursive copy
if ( currentDepth >= 255 ) return
currentDepth ++
2019-05-22 20:05:34 +00:00
2019-07-09 13:56:01 +00:00
await mkdirP ( destDir )
2019-05-22 20:05:34 +00:00
2019-07-09 13:56:01 +00:00
const files : string [ ] = await ioUtil . readdir ( sourceDir )
2019-05-22 20:05:34 +00:00
2019-07-09 13:56:01 +00:00
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 )
2019-05-22 20:05:34 +00:00
} else {
2019-07-09 13:56:01 +00:00
await copyFile ( srcFile , destFile , force )
2019-05-22 20:05:34 +00:00
}
}
2019-07-09 13:56:01 +00:00
// Change the mode for the newly created directory
await ioUtil . chmod ( destDir , ( await ioUtil . stat ( sourceDir ) ) . mode )
2019-05-22 20:05:34 +00:00
}
2019-07-09 13:56:01 +00:00
// Buffered file copy
async function copyFile (
srcFile : string ,
destFile : string ,
force : boolean
2019-05-22 20:05:34 +00:00
) : Promise < void > {
2019-07-09 13:56:01 +00:00
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 )
2019-05-22 20:05:34 +00:00
}
2019-07-09 13:56:01 +00:00
// other errors = it doesn't exist, no work to do
2019-05-22 20:05:34 +00:00
}
2019-07-09 13:56:01 +00:00
// 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 )
2019-05-22 20:05:34 +00:00
}
}