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 )
/ * *
* 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
}
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 > {
2019-07-09 13:56:01 +00:00
const { force , recursive } = readCopyOptions ( options )
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 =
destStat && destStat . isDirectory ( )
? 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.
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
//
2019-08-21 16:54:24 +00:00
// Note, technically "where" checks the current directory on Windows. From a toolkit perspective,
2019-05-22 20:05:34 +00:00
// it feels like we should not do this. Checking the current directory seems like more of a use
2019-08-21 16:54:24 +00:00
// case of a shell, and the which() function exposed by the toolkit should strive for consistency
2019-05-22 20:05:34 +00:00
// 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 } ` )
}
}
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 )
return { force , recursive }
}
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
}
}