var fs = require('fs'); var path = require('path'); var common = require('./common'); common.register('cp', _cp, { cmdOptions: { 'f': '!no_force', 'n': 'no_force', 'u': 'update', 'R': 'recursive', 'r': 'recursive', 'L': 'followsymlink', 'P': 'noFollowsymlink', }, wrapOutput: false, }); // Buffered file copy, synchronous // (Using readFileSync() + writeFileSync() could easily cause a memory overflow // with large files) function copyFileSync(srcFile, destFile, options) { if (!fs.existsSync(srcFile)) { common.error('copyFileSync: no such file or directory: ' + srcFile); } var isWindows = process.platform === 'win32'; // Check the mtimes of the files if the '-u' flag is provided try { if (options.update && fs.statSync(srcFile).mtime < fs.statSync(destFile).mtime) { return; } } catch (e) { // If we're here, destFile probably doesn't exist, so just do a normal copy } if (fs.lstatSync(srcFile).isSymbolicLink() && !options.followsymlink) { try { fs.lstatSync(destFile); common.unlinkSync(destFile); // re-link it } catch (e) { // it doesn't exist, so no work needs to be done } var symlinkFull = fs.readlinkSync(srcFile); fs.symlinkSync(symlinkFull, destFile, isWindows ? 'junction' : null); } else { var buf = common.buffer(); var bufLength = buf.length; var bytesRead = bufLength; var pos = 0; var fdr = null; var fdw = null; try { fdr = fs.openSync(srcFile, 'r'); } catch (e) { /* istanbul ignore next */ common.error('copyFileSync: could not read src file (' + srcFile + ')'); } try { fdw = fs.openSync(destFile, 'w'); } catch (e) { /* istanbul ignore next */ common.error('copyFileSync: could not write to dest file (code=' + e.code + '):' + destFile); } while (bytesRead === bufLength) { bytesRead = fs.readSync(fdr, buf, 0, bufLength, pos); fs.writeSync(fdw, buf, 0, bytesRead); pos += bytesRead; } fs.closeSync(fdr); fs.closeSync(fdw); fs.chmodSync(destFile, fs.statSync(srcFile).mode); } } // Recursively copies 'sourceDir' into 'destDir' // Adapted from https://github.com/ryanmcgrath/wrench-js // // Copyright (c) 2010 Ryan McGrath // Copyright (c) 2012 Artur Adib // // Licensed under the MIT License // http://www.opensource.org/licenses/mit-license.php function cpdirSyncRecursive(sourceDir, destDir, currentDepth, opts) { if (!opts) opts = {}; // Ensure there is not a run away recursive copy if (currentDepth >= common.config.maxdepth) return; currentDepth++; var isWindows = process.platform === 'win32'; // Create the directory where all our junk is moving to; read the mode of the // source directory and mirror it try { var checkDir = fs.statSync(sourceDir); fs.mkdirSync(destDir, checkDir.mode); } catch (e) { // if the directory already exists, that's okay if (e.code !== 'EEXIST') throw e; } var files = fs.readdirSync(sourceDir); for (var i = 0; i < files.length; i++) { var srcFile = sourceDir + '/' + files[i]; var destFile = destDir + '/' + files[i]; var srcFileStat = fs.lstatSync(srcFile); var symlinkFull; if (opts.followsymlink) { if (cpcheckcycle(sourceDir, srcFile)) { // Cycle link found. console.error('Cycle link found.'); symlinkFull = fs.readlinkSync(srcFile); fs.symlinkSync(symlinkFull, destFile, isWindows ? 'junction' : null); continue; } } if (srcFileStat.isDirectory()) { /* recursion this thing right on back. */ cpdirSyncRecursive(srcFile, destFile, currentDepth, opts); } else if (srcFileStat.isSymbolicLink() && !opts.followsymlink) { symlinkFull = fs.readlinkSync(srcFile); try { fs.lstatSync(destFile); common.unlinkSync(destFile); // re-link it } catch (e) { // it doesn't exist, so no work needs to be done } fs.symlinkSync(symlinkFull, destFile, isWindows ? 'junction' : null); } else if (srcFileStat.isSymbolicLink() && opts.followsymlink) { srcFileStat = fs.statSync(srcFile); if (srcFileStat.isDirectory()) { cpdirSyncRecursive(srcFile, destFile, currentDepth, opts); } else { copyFileSync(srcFile, destFile, opts); } } else { /* At this point, we've hit a file actually worth copying... so copy it on over. */ if (fs.existsSync(destFile) && opts.no_force) { common.log('skipping existing file: ' + files[i]); } else { copyFileSync(srcFile, destFile, opts); } } } // for files } // cpdirSyncRecursive // Checks if cureent file was created recently function checkRecentCreated(sources, index) { var lookedSource = sources[index]; return sources.slice(0, index).some(function (src) { return path.basename(src) === path.basename(lookedSource); }); } function cpcheckcycle(sourceDir, srcFile) { var srcFileStat = fs.lstatSync(srcFile); if (srcFileStat.isSymbolicLink()) { // Do cycle check. For example: // $ mkdir -p 1/2/3/4 // $ cd 1/2/3/4 // $ ln -s ../../3 link // $ cd ../../../.. // $ cp -RL 1 copy var cyclecheck = fs.statSync(srcFile); if (cyclecheck.isDirectory()) { var sourcerealpath = fs.realpathSync(sourceDir); var symlinkrealpath = fs.realpathSync(srcFile); var re = new RegExp(symlinkrealpath); if (re.test(sourcerealpath)) { return true; } } } return false; } //@ //@ ### cp([options,] source [, source ...], dest) //@ ### cp([options,] source_array, dest) //@ Available options: //@ //@ + `-f`: force (default behavior) //@ + `-n`: no-clobber //@ + `-u`: only copy if source is newer than dest //@ + `-r`, `-R`: recursive //@ + `-L`: follow symlinks //@ + `-P`: don't follow symlinks //@ //@ Examples: //@ //@ ```javascript //@ cp('file1', 'dir1'); //@ cp('-R', 'path/to/dir/', '~/newCopy/'); //@ cp('-Rf', '/tmp/*', '/usr/local/*', '/home/tmp'); //@ cp('-Rf', ['/tmp/*', '/usr/local/*'], '/home/tmp'); // same as above //@ ``` //@ //@ Copies files. function _cp(options, sources, dest) { // If we're missing -R, it actually implies -L (unless -P is explicit) if (options.followsymlink) { options.noFollowsymlink = false; } if (!options.recursive && !options.noFollowsymlink) { options.followsymlink = true; } // Get sources, dest if (arguments.length < 3) { common.error('missing and/or '); } else { sources = [].slice.call(arguments, 1, arguments.length - 1); dest = arguments[arguments.length - 1]; } var destExists = fs.existsSync(dest); var destStat = destExists && fs.statSync(dest); // Dest is not existing dir, but multiple sources given if ((!destExists || !destStat.isDirectory()) && sources.length > 1) { common.error('dest is not a directory (too many sources)'); } // Dest is an existing file, but -n is given if (destExists && destStat.isFile() && options.no_force) { return new common.ShellString('', '', 0); } sources.forEach(function (src, srcIndex) { if (!fs.existsSync(src)) { if (src === '') src = "''"; // if src was empty string, display empty string common.error('no such file or directory: ' + src, { continue: true }); return; // skip file } var srcStat = fs.statSync(src); if (!options.noFollowsymlink && srcStat.isDirectory()) { if (!options.recursive) { // Non-Recursive common.error("omitting directory '" + src + "'", { continue: true }); } else { // Recursive // 'cp /a/source dest' should create 'source' in 'dest' var newDest = (destStat && destStat.isDirectory()) ? path.join(dest, path.basename(src)) : dest; try { fs.statSync(path.dirname(dest)); cpdirSyncRecursive(src, newDest, 0, { no_force: options.no_force, followsymlink: options.followsymlink }); } catch (e) { /* istanbul ignore next */ common.error("cannot create directory '" + dest + "': No such file or directory"); } } } else { // If here, src is a file // When copying to '/path/dir': // thisDest = '/path/dir/file1' var thisDest = dest; if (destStat && destStat.isDirectory()) { thisDest = path.normalize(dest + '/' + path.basename(src)); } var thisDestExists = fs.existsSync(thisDest); if (thisDestExists && checkRecentCreated(sources, srcIndex)) { // cannot overwrite file created recently in current execution, but we want to continue copying other files if (!options.no_force) { common.error("will not overwrite just-created '" + thisDest + "' with '" + src + "'", { continue: true }); } return; } if (thisDestExists && options.no_force) { return; // skip file } if (path.relative(src, thisDest) === '') { // a file cannot be copied to itself, but we want to continue copying other files common.error("'" + thisDest + "' and '" + src + "' are the same file", { continue: true }); return; } copyFileSync(src, thisDest, options); } }); // forEach(src) return new common.ShellString('', common.state.error, common.state.errorCode); } module.exports = _cp;