在Node.js中递归复制文件夹

有没有一种更简单的方法来复制文件夹及其所有内容,而无需手动递归地执行fs.readirfs.readfilefs.writefile的序列?

我只是想知道我是否错过了一个函数,理想情况下是这样工作的:

fs.copy("/path/to/source/folder", "/path/to/destination/folder");

关于这个历史问题。注意,fs.cpfs.cpSync可以递归复制文件夹,在Node v16+中可用

284238 次浏览

有一些模块支持复制文件夹及其内容。最流行的是wrench.js:

// Deep-copy an existing directory
wrench.copyDirSyncRecursive('directory_to_copy', 'location_where_copy_should_end_up');

另一种方法是node-fs-extra:

fs.copy('/tmp/mydir', '/tmp/mynewdir', function (err) {
if (err) {
console.error(err);
} else {
console.log("success!");
}
}); // Copies directory, even if it has subdirectories or files

由于我只是构建一个简单的Node.js脚本,我不希望脚本的用户需要导入一堆外部模块和依赖项,所以我开始思考,并从Bash shell中搜索运行命令。

这个Node.js代码片段递归地复制了一个名为node-webkit的文件夹。应用程序到一个名为build的文件夹:

child = exec("cp -r node-webkit.app build", function(error, stdout, stderr) {
sys.print("stdout: " + stdout);
sys.print("stderr: " + stderr);
if(error !== null) {
console.log("exec error: " + error);
} else {


}
});

感谢兰斯·波拉德在dzone报道让我开始。

上面的代码片段仅限于基于unix的平台,如macOS和Linux,但类似的技术也适用于Windows。

下面是一个递归复制目录及其内容到另一个目录的函数:

const fs = require("fs")
const path = require("path")


/**
* Look ma, it's cp -R.
* @param {string} src  The path to the thing to copy.
* @param {string} dest The path to the new copy.
*/
var copyRecursiveSync = function(src, dest) {
var exists = fs.existsSync(src);
var stats = exists && fs.statSync(src);
var isDirectory = exists && stats.isDirectory();
if (isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function(childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
fs.copyFileSync(src, dest);
}
};

这是我解决这个问题的方法,没有任何额外的模块。只使用内置的fspath模块。

注意:它使用fs的读/写函数,所以它不复制任何元数据(创建时间等)。从Node.js 8.5开始,有一个copyFileSync函数可用,它调用操作系统复制函数,因此也复制元数据。我还没有测试它们,但它应该可以替换它们。(见https://nodejs.org/api/fs.html#fs_fs_copyfilesync_src_dest_flags)

var fs = require('fs');
var path = require('path');


function copyFileSync( source, target ) {


var targetFile = target;


// If target is a directory, a new file with the same name will be created
if ( fs.existsSync( target ) ) {
if ( fs.lstatSync( target ).isDirectory() ) {
targetFile = path.join( target, path.basename( source ) );
}
}


fs.writeFileSync(targetFile, fs.readFileSync(source));
}


function copyFolderRecursiveSync( source, target ) {
var files = [];


// Check if folder needs to be created or integrated
var targetFolder = path.join( target, path.basename( source ) );
if ( !fs.existsSync( targetFolder ) ) {
fs.mkdirSync( targetFolder );
}


// Copy
if ( fs.lstatSync( source ).isDirectory() ) {
files = fs.readdirSync( source );
files.forEach( function ( file ) {
var curSource = path.join( source, file );
if ( fs.lstatSync( curSource ).isDirectory() ) {
copyFolderRecursiveSync( curSource, targetFolder );
} else {
copyFileSync( curSource, targetFolder );
}
} );
}
}

这段代码可以很好地工作,递归地将任何文件夹复制到任何位置。但它只适用于Windows。

var child = require("child_process");
function copySync(from, to){
from = from.replace(/\//gim, "\\");
to = to.replace(/\//gim, "\\");
child.exec("xcopy /y /q \"" + from + "\\*\" \"" + to + "\\\"");
}

它非常适合我的基于文本的游戏去创造新玩家。

我创建了一个小的工作示例,只需几个步骤就可以将源文件夹复制到另一个目标文件夹(基于shift66的回答使用ncp):

步骤1 -安装ncp模块:

npm install ncp --save

步骤2 -创建copy.js(修改srcPathdestPath变量为你需要的任何变量):

var path = require('path');
var ncp = require('ncp').ncp;


ncp.limit = 16;


var srcPath = path.dirname(require.main.filename); // Current folder
var destPath = '/path/to/destination/folder'; // Any destination folder


console.log('Copying files...');
ncp(srcPath, destPath, function (err) {
if (err) {
return console.error(err);
}
console.log('Copying files complete.');
});

第三步——跑步

node copy.js

对于Linux/Unix操作系统,可以使用shell语法

const shell = require('child_process').execSync;


const src = `/path/src`;
const dist = `/path/dist`;


shell(`mkdir -p ${dist}`);
shell(`cp -r ${src}/* ${dist}`);

就是这样!

fs-extra模块工作起来很有魅力。

安装fs-extra:

$ npm install fs-extra

下面是将源目录复制到目标目录的程序。

// Include the fs-extra package
var fs = require("fs-extra");


var source = 'folderA'
var destination = 'folderB'


// Copy the source folder to the destination
fs.copy(source, destination, function (err) {
if (err){
console.log('An error occurred while copying the folder.')
return console.error(err)
}
console.log('Copy completed!')
});

参考文献

fs-extra: https://www.npmjs.com/package/fs-extra

例如:node . js教程 - 复制一个文件夹

是的,ncpcool

你可能想要/应该将其函数赋值为super cool。当你这样做的时候,将它添加到tools文件中以重用它。

下面是一个工作版本,它是Async,并使用了Promises


文件index.js

const {copyFolder} = require('./tools/');


return copyFolder(
yourSourcePath,
yourDestinationPath
)
.then(() => {
console.log('-> Backup completed.')
}) .catch((err) => {
console.log("-> [ERR] Could not copy the folder: ", err);
})

文件tools.js

const ncp = require("ncp");


/**
* Promise Version of ncp.ncp()
*
* This function promisifies ncp.ncp().
* We take the asynchronous function ncp.ncp() with
* callback semantics and derive from it a new function with
* promise semantics.
*/
ncp.ncpAsync = function (sourcePath, destinationPath) {
return new Promise(function (resolve, reject) {
try {
ncp.ncp(sourcePath, destinationPath, function(err){
if (err) reject(err); else resolve();
});
} catch (err) {
reject(err);
}
});
};


/**
* Utility function to copy folders asynchronously using
* the Promise returned by ncp.ncp().
*/
const copyFolder = (sourcePath, destinationPath) => {
return ncp.ncpAsync(sourcePath, destinationPath, function (err) {
if (err) {
return console.error(err);
}
});
}
module.exports.copyFolder = copyFolder;

Mallikarjun M,谢谢 !

fs-extra做了这件事,如果你不提供回调,它甚至可以返回承诺 !:)

const path = require('path')
const fs = require('fs-extra')


let source = path.resolve( __dirname, 'folderA')
let destination = path.resolve( __dirname, 'folderB')


fs.copy(source, destination)
.then(() => console.log('Copy completed!'))
.catch( err => {
console.log('An error occurred while copying the folder.')
return console.error(err)
})

解决这个问题最简单的方法是只使用'fs'和'Path'模块和一些逻辑…

如果你只是想设置版本号,即“;var v = '您的目录名';,根文件夹中的所有文件都复制新名字

在文件名的前缀中添加带有文件名的内容。

var fs = require('fs-extra');
var path = require('path');


var c = 0;
var i = 0;
var v = "1.0.2";
var copyCounter = 0;
var directoryCounter = 0;
var directoryMakerCounter = 0;
var recursionCounter = -1;
var Flag = false;
var directoryPath = [];
var directoryName = [];
var directoryFileName = [];
var fileName;
var directoryNameStorer;
var dc = 0;
var route;


if (!fs.existsSync(v)) {
fs.mkdirSync(v);
}


var basePath = path.join(__dirname, v);




function walk(dir) {


fs.readdir(dir, function(err, items) {


items.forEach(function(file) {


file = path.resolve(dir, file);


fs.stat(file, function(err, stat) {


if(stat && stat.isDirectory()) {
directoryNameStorer = path.basename(file);
route = file;
route = route.replace("gd", v);


directoryFileName[directoryCounter] = route;
directoryPath[directoryCounter] = file;
directoryName[directoryCounter] = directoryNameStorer;


directoryCounter++;
dc++;


if (!fs.existsSync(basePath + "/" + directoryName[directoryMakerCounter])) {
fs.mkdirSync(directoryFileName[directoryMakerCounter]);
directoryMakerCounter++;
}
}
else {
fileName = path.basename(file);
if(recursionCounter >= 0) {
fs.copyFileSync(file, directoryFileName[recursionCounter] + "/" + v + "_" + fileName, err => {
if(err) return console.error(err);
});
copyCounter++;
}
else {
fs.copyFileSync(file, v + "/" + v + "_" + fileName, err => {
if(err) return console.error(err);
});
copyCounter++;
}
}
if(copyCounter + dc == items.length && directoryCounter > 0 && recursionCounter < directoryMakerCounter-1) {
console.log("COPY COUNTER:             " + copyCounter);
console.log("DC COUNTER:               " + dc);
recursionCounter++;
dc = 0;
copyCounter = 0;
console.log("ITEM DOT LENGTH:          " + items.length);
console.log("RECURSION COUNTER:        " + recursionCounter);
console.log("DIRECOTRY MAKER COUNTER:  " + directoryMakerCounter);
console.log(": START RECURSION:        " + directoryPath[recursionCounter]);
walk(directoryPath[recursionCounter]); //recursive call to copy sub-folder
}
})
})
});
}


walk('./gd', function(err, data) { // Just pass the root directory which you want to copy
if(err)
throw err;
console.log("done");
})

这是我个人的做法:

function copyFolderSync(from, to) {
fs.mkdirSync(to);
fs.readdirSync(from).forEach(element => {
if (fs.lstatSync(path.join(from, element)).isFile()) {
fs.copyFileSync(path.join(from, element), path.join(to, element));
} else {
copyFolderSync(path.join(from, element), path.join(to, element));
}
});
}

它适用于文件夹和文件。

这在Node.js 10中非常简单:

const Path = require('path');
const FSP = require('fs').promises;


async function copyDir(src,dest) {
const entries = await FSP.readdir(src, {withFileTypes: true});
await FSP.mkdir(dest);
for(let entry of entries) {
const srcPath = Path.join(src, entry.name);
const destPath = Path.join(dest, entry.name);
if(entry.isDirectory()) {
await copyDir(srcPath, destPath);
} else {
await FSP.copyFile(srcPath, destPath);
}
}
}

这假设dest不存在。

我是这样做的:

let fs = require('fs');
let path = require('path');

然后:

let filePath = // Your file path


let fileList = []
var walkSync = function(filePath, filelist)
{
let files = fs.readdirSync(filePath);
filelist = filelist || [];
files.forEach(function(file)
{
if (fs.statSync(path.join(filePath, file)).isDirectory())
{
filelist = walkSync(path.join(filePath, file), filelist);
}
else
{
filelist.push(path.join(filePath, file));
}
});


// Ignore hidden files
filelist = filelist.filter(item => !(/(^|\/)\.[^\/\.]/g).test(item));


return filelist;
};

然后调用该方法:

This.walkSync(filePath, fileList)

我尝试了fs-extra和copy-dir来递归地复制文件夹。但我希望它能

  1. 正常工作(copy-dir抛出一个不合理的错误)
  2. 在过滤器中提供两个参数:filepath和filetype (fs-extra不告诉文件类型)
  3. 有从目录到子目录的检查和从目录到文件的检查吗

所以我自己写了:

// Node.js module for Node.js 8.6+
var path = require("path");
var fs = require("fs");


function copyDirSync(src, dest, options) {
var srcPath = path.resolve(src);
var destPath = path.resolve(dest);
if(path.relative(srcPath, destPath).charAt(0) != ".")
throw new Error("dest path must be out of src path");
var settings = Object.assign(Object.create(copyDirSync.options), options);
copyDirSync0(srcPath, destPath, settings);
function copyDirSync0(srcPath, destPath, settings) {
var files = fs.readdirSync(srcPath);
if (!fs.existsSync(destPath)) {
fs.mkdirSync(destPath);
}else if(!fs.lstatSync(destPath).isDirectory()) {
if(settings.overwrite)
throw new Error(`Cannot overwrite non-directory '${destPath}' with directory '${srcPath}'.`);
return;
}
files.forEach(function(filename) {
var childSrcPath = path.join(srcPath, filename);
var childDestPath = path.join(destPath, filename);
var type = fs.lstatSync(childSrcPath).isDirectory() ? "directory" : "file";
if(!settings.filter(childSrcPath, type))
return;
if (type == "directory") {
copyDirSync0(childSrcPath, childDestPath, settings);
} else {
fs.copyFileSync(childSrcPath, childDestPath, settings.overwrite ? 0 : fs.constants.COPYFILE_EXCL);
if(!settings.preserveFileDate)
fs.futimesSync(childDestPath, Date.now(), Date.now());
}
});
}
}
copyDirSync.options = {
overwrite: true,
preserveFileDate: true,
filter: function(filepath, type) {
return true;
}
};

还有一个类似的函数mkdir,它是mkdirp的替代:

function mkdirsSync(dest) {
var destPath = path.resolve(dest);
mkdirsSync0(destPath);
function mkdirsSync0(destPath) {
var parentPath = path.dirname(destPath);
if(parentPath == destPath)
throw new Error(`cannot mkdir ${destPath}, invalid root`);
if (!fs.existsSync(destPath)) {
mkdirsSync0(parentPath);
fs.mkdirSync(destPath);
}else if(!fs.lstatSync(destPath).isDirectory()) {
throw new Error(`cannot mkdir ${destPath}, a file already exists there`);
}
}
}

我写了这个函数用于在目录之间递归地复制(copyFileSync)或移动(renameSync)文件:

// Copy files
copyDirectoryRecursiveSync(sourceDir, targetDir);
// Move files
copyDirectoryRecursiveSync(sourceDir, targetDir, true);




function copyDirectoryRecursiveSync(source, target, move) {
if (!fs.lstatSync(source).isDirectory())
return;


var operation = move ? fs.renameSync : fs.copyFileSync;
fs.readdirSync(source).forEach(function (itemName) {
var sourcePath = path.join(source, itemName);
var targetPath = path.join(target, itemName);


if (fs.lstatSync(sourcePath).isDirectory()) {
fs.mkdirSync(targetPath);
copyDirectoryRecursiveSync(sourcePath, targetPath);
}
else {
operation(sourcePath, targetPath);
}
});
}

我知道这里已经有很多答案了,但是没有一个答案是简单的。

对于fs-exra 官方文档,你可以很容易地做到。

const fs = require('fs-extra')


// Copy file
fs.copySync('/tmp/myfile', '/tmp/mynewfile')


// Copy directory, even if it has subdirectories or files
fs.copySync('/tmp/mydir', '/tmp/mynewdir')


如果你在Linux上,性能不是问题,你可以使用child_process模块中的exec函数来执行Bash命令:

const { exec } = require('child_process');
exec('cp -r source dest', (error, stdout, stderr) => {...});

在某些情况下,我发现这个解决方案比下载整个模块或甚至使用fs模块更干净。

挑选包裹时要小心。有些包(如copy-dir)不支持复制长度超过0X1FFFFFE8个字符(约537 MB)的大文件。

它会抛出一些错误,比如:

buffer.js:630 Uncaught Error:不能创建大于0x1fffffe8字符的字符串

在我的一个项目中,我就经历过类似的事情。最终,我不得不改变我正在使用的包并调整大量代码。我想说,这不是一个很愉快的经历。

如果需要多个源副本和多个目标副本,可以使用高明并编写如下内容:

// Copy from multiple source into a directory
bCopy(['/path/to/your/folder1', '/path/to/some/file.txt'], '/path/to/destination/folder');

甚至:

// Copy from multiple source into multiple destination
bCopy(['/path/to/your/folder1', '/path/to/some/file.txt'], ['/path/to/destination/folder', '/path/to/another/folder']);

符号链接支持的那个:

const path = require("path");
const {
existsSync,
mkdirSync,
readdirSync,
lstatSync,
copyFileSync,
symlinkSync,
readlinkSync,
} = require("fs");


export function copyFolderSync(src, dest) {
if (!existsSync(dest)) {
mkdirSync(dest);
}


readdirSync(src).forEach((entry) => {
const srcPath = path.join(src, entry);
const destPath = path.join(dest, entry);
const stat = lstatSync(srcPath);


if (stat.isFile()) {
copyFileSync(srcPath, destPath);
} else if (stat.isDirectory()) {
copyFolderSync(srcPath, destPath);
} else if (stat.isSymbolicLink()) {
symlinkSync(readlinkSync(srcPath), destPath);
}
});
}

看起来< >强ncp < / >强< >强扳手< / >强都不再被维护。可能最好的选择是使用< >强fs-extra < / >强

扳手的开发人员指导用户使用fs-extra,因为他已经弃用了他的库

copySync,moveSync都将复制和移动文件夹,即使他们有文件或子文件夹,你可以很容易地移动或复制文件使用它

const fse = require('fs-extra');


const srcDir = `path/to/file`;
const destDir = `path/to/destination/directory`;
                                 

// To copy a folder or file, select overwrite accordingly
try {
fs.copySync(srcDir, destDir, { overwrite: true|false })
console.log('success!')
} catch (err) {
console.error(err)
}

<强>或< / >强

// To Move a folder or file, select overwrite accordingly
try {
fs.moveSync(srcDir, destDir, { overwrite: true|false })
console.log('success!')
} catch (err) {
console.error(err)
}

如果你想递归复制源目录的所有内容,那么你需要传递recursive选项作为truetry catch是通过fs-extra为sync记录的方式

因为fs-extra完全取代了fs,所以你不需要导入基本模块

const fs = require('fs-extra');
let sourceDir = '/tmp/src_dir';
let destDir = '/tmp/dest_dir';
try {
fs.copySync(sourceDir, destDir, { recursive: true })
console.log('success!')
} catch (err) {
console.error(err)
}


使用shelljs

npm i -D shelljs

const bash = require('shelljs');
bash.cp("-rf", "/path/to/source/folder", "/path/to/destination/folder");

从Node v16.7.0开始,可以使用fs.cpfs.cpSync函数。

fs.cp(src, dest, {recursive: true});

当前稳定性(在节点v18.7.0中)为实验

目前最上面的答案可以大大简化。

const path = require('path');
const fs = require('fs');


function recursiveCopySync(source, target) {
if (fs.lstatSync(source).isDirectory()) {
if (!fs.existsSync(target)) {
fs.mkdirSync(target);
}
let files = fs.readdirSync(source);
files.forEach((file) => {
recursiveCopySync(path.join(source, file), path.join(target, file));
});
} else {
if (fs.existsSync(source)) {
fs.writeFileSync(target, fs.readFileSync(source));
}
}
}

打印稿版本

async function copyDir(source: string, destination: string): Promise<any> {
const directoryEntries = await readdir(source, { withFileTypes: true });
await mkdir(destination, { recursive: true });


return Promise.all(
directoryEntries.map(async (entry) => {
const sourcePath = path.join(source, entry.name);
const destinationPath = path.join(destination, entry.name);


return entry.isDirectory()
? copyDir(sourcePath, destinationPath)
: copyFile(sourcePath, destinationPath);
})
);
}

从Node v16.7.0开始:

import { cp } from 'fs/promises';
await cp(
new URL('../path/to/src/', import.meta.url),
new URL('../path/to/dest/', import.meta.url), {
recursive: true,
}
);

仔细注意recursive: true的使用。这可以防止ERR_FS_EISDIR错误。

阅读更多关于节点上的文件系统文档的内容

对于没有fs.cp的旧节点版本,我在紧要关头使用这个来避免需要第三方库:

const fs = require("fs").promises;
const path = require("path");


const cp = async (src, dest) => {
const lstat = await fs.lstat(src).catch(err => false);


if (!lstat) {
return;
}
else if (await lstat.isFile()) {
await fs.copyFile(src, dest);
}
else if (await lstat.isDirectory()) {
await fs.mkdir(dest).catch(err => {});


for (const f of await fs.readdir(src)) {
await cp(path.join(src, f), path.join(dest, f));
}
}
};


// sample usage
(async () => {
const src = "foo";
const dst = "bar";


for (const f of await fs.readdir(src)) {
await cp(path.join(src, f), path.join(dst, f));
}
})();

相对于现有答案的优势(或区别):

  • 异步
  • 忽略符号链接
  • 如果目录已经存在,则不会抛出(如果不需要,则不捕获mkdir抛出)
  • 相当简洁的

这可能是一个可能的解决方案使用异步生成器函数和迭代for await循环。这个解决方案包括过滤掉一些目录的可能性,将它们作为可选的第三个数组参数传递。

import path from 'path';
import { readdir, copy } from 'fs-extra';


async function* getFilesRecursive(srcDir: string, excludedDir?: PathLike[]): AsyncGenerator<string> {
const directoryEntries: Dirent[] = await readdir(srcDir, { withFileTypes: true });
if (!directoryEntries.length) yield srcDir; // If the directory is empty, return the directory path.
for (const entry of directoryEntries) {
const fileName = entry.name;
const sourcePath = resolvePath(`${srcDir}/${fileName}`);
if (entry.isDirectory()) {
if (!excludedDir?.includes(sourcePath)) {
yield* getFilesRecursive(sourcePath, excludedDir);
}
} else {
yield sourcePath;
}
}
}

然后:

for await (const filePath of getFilesRecursive(path, ['dir1', 'dir2'])) {
await copy(filePath, filePath.replace(path, path2));
}