├── bin └── dfc.js ├── src ├── index.ts ├── config.ts ├── parseConfig.spec.ts ├── worker.ts ├── dir-rm.ts ├── parseConfig.ts ├── type.ts ├── cli.ts ├── fast-copy.ts └── utils.ts ├── docs ├── dfc-cp-2.png ├── dfc-cp-thread-1.png ├── dfc-cp-thread-3.png └── dfc-cp-thread-8.png ├── .prettierignore ├── .husky ├── pre-commit └── commit-msg ├── .gitignore ├── .editorconfig ├── .npmignore ├── benchmark ├── utils.ts ├── dfc.ts └── test-fs.ts ├── LICENSE ├── scripts └── verifyCommit.js ├── tsconfig.json ├── .github └── workflows │ └── npm-publish.yml ├── CHANGELOG.md ├── package.json └── README.md /bin/dfc.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/cli'); 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fast-copy'; 2 | export * from './dir-rm'; 3 | -------------------------------------------------------------------------------- /docs/dfc-cp-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzwme/dir-fast-copy/HEAD/docs/dfc-cp-2.png -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # package.json is formatted by package managers, so we ignore it here 2 | package.json -------------------------------------------------------------------------------- /docs/dfc-cp-thread-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzwme/dir-fast-copy/HEAD/docs/dfc-cp-thread-1.png -------------------------------------------------------------------------------- /docs/dfc-cp-thread-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzwme/dir-fast-copy/HEAD/docs/dfc-cp-thread-3.png -------------------------------------------------------------------------------- /docs/dfc-cp-thread-8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lzwme/dir-fast-copy/HEAD/docs/dfc-cp-thread-8.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install lint-staged 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | node scripts/verifyCommit.js $1 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | test 5 | tmp 6 | 7 | src/**.js 8 | .idea/* 9 | 10 | coverage 11 | .nyc_output 12 | *.log 13 | package-lock.json 14 | yarn.lock 15 | bat 16 | pnpm-lock.yaml 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 140 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | unit 3 | test 4 | tsconfig.json 5 | tsconfig.module.json 6 | tslint.json 7 | .travis.yml 8 | .gitlab-ci.yml 9 | .circleci/* 10 | .github 11 | .prettierignore 12 | .vscode 13 | build 14 | **/*.spec.* 15 | coverage 16 | .nyc_output 17 | *.log 18 | package-lock.json 19 | yarn.lock 20 | pnpm-lock.yaml 21 | commitlint.config.js 22 | .editorconfig 23 | publish.bat 24 | bat 25 | *.bat 26 | .prettierignore 27 | .gitignore 28 | scripts 29 | docs 30 | .husky 31 | CHANGELOG.md 32 | benchmark 33 | tmp 34 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { cpus } from 'os'; 2 | import { type DfcConfig } from './type'; 3 | 4 | export const CONFIG: DfcConfig = { 5 | slient: false, 6 | iscmd: false, 7 | src: '', 8 | dest: '', 9 | threads: Math.max(cpus().length - 1, 1), 10 | mutiThreadMinFiles: 3000, 11 | exclude: [], // [/\.pyc$/], 12 | minDateTime: 0, // new Date('1970-01-01T00:00:00').getTime(), 13 | skipSameFile: true, 14 | progressInterval: 2000, 15 | /** 结束时回调方法 */ 16 | onEnd: null, 17 | onProgress: null, 18 | }; 19 | 20 | export default CONFIG; 21 | -------------------------------------------------------------------------------- /benchmark/utils.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import { resolve } from "path"; 3 | 4 | export const rootDir = resolve(__dirname, '..'); 5 | 6 | export function runCmd(cmd: string, debug = false) { 7 | return execSync(cmd, { encoding: 'utf8', stdio: debug ? 'inherit' : 'pipe' }); 8 | } 9 | 10 | export async function calcTimeCost(fn, label = '') { 11 | const startTime = process.hrtime.bigint(); 12 | await fn(); 13 | const endTime = process.hrtime.bigint(); 14 | const timeCost = Number(endTime - startTime); 15 | 16 | console.log(`[${label || fn.name}] timeCost:`, timeCost / 1e9, 's'); 17 | 18 | return timeCost; 19 | } 20 | -------------------------------------------------------------------------------- /src/parseConfig.spec.ts: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | import { parseConfig } from './parseConfig'; 3 | 4 | test('parseConfig', (t) => { 5 | const a = parseConfig({ src: null, dest: null }); 6 | t.is(a, void 0, '未指定源目录'); 7 | 8 | const b = parseConfig({ 9 | src: 'test' + Math.random(), 10 | dest: 'test', 11 | }); 12 | 13 | t.is(b, void 0, '源目录不存在'); 14 | 15 | const c = parseConfig({ 16 | src: './dist', 17 | dest: './test/dist-dest', 18 | }); 19 | 20 | t.is(c !== void 0, true); 21 | 22 | const d = parseConfig({ 23 | src: './dist', 24 | dest: './test/dist-dest', 25 | exclude: ['test/**', /test/], 26 | }); 27 | 28 | t.is(d !== void 0, true, '定义文件排除规则'); 29 | }); 30 | -------------------------------------------------------------------------------- /benchmark/dfc.ts: -------------------------------------------------------------------------------- 1 | import { mkdirSync, rmSync } from "fs"; 2 | import { resolve } from "path"; 3 | import { runCmd, calcTimeCost } from "./utils"; 4 | 5 | const rootDir = resolve(__dirname, '..'); 6 | const src = resolve(rootDir, `node_modules`); 7 | 8 | function dfc(threads = 1, debug = false) { 9 | const dest = resolve(rootDir, `tmp/dfc-threads-${threads}-nm`); 10 | runCmd(`node bin/dfc.js cp --threads=${threads} "${src}" "${dest}"`); 11 | } 12 | function xcopy(debug = false) { 13 | if (process.platform !== 'win32') return; 14 | const dest = resolve(rootDir, `tmp/xcopy-nm`); 15 | mkdirSync(dest, { recursive: true }); 16 | runCmd(`XCOPY "${src}" "${dest}" /E /Y` + (debug ? '' : ' /Q')); 17 | } 18 | 19 | async function testStart() { 20 | rmSync('tmp', { recursive: true }); 21 | 22 | // for xcopy 23 | await calcTimeCost(() => xcopy(), 'xcopy'); 24 | 25 | // for dfc 26 | const threadList = [1, 2, 4, 8]; 27 | for (const thread of threadList) { 28 | await calcTimeCost(() => dfc(thread), `dfc-thread-${thread}`); 29 | } 30 | } 31 | 32 | testStart(); 33 | -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lzw 3 | * @Date: 2020-09-18 09:52:53 4 | * @LastEditors: lzw 5 | * @LastEditTime: 2020-09-22 21:23:51 6 | * @Description: 子线程 worker 7 | */ 8 | import { parentPort, workerData, isMainThread } from 'worker_threads'; 9 | import { fileCopy } from './utils'; 10 | import CONFIG from './config'; 11 | 12 | /** 启动子线程 */ 13 | function startChild() { 14 | if (workerData.config) Object.assign(CONFIG, workerData.config); 15 | 16 | // startTime = workerData.startTime; 17 | 18 | fileCopy(workerData.filePathList, { 19 | onProgress: (stats) => { 20 | parentPort.postMessage({ 21 | type: 'progress', 22 | idx: workerData.idx, 23 | ...stats, 24 | }); 25 | }, 26 | onEnd: (stats) => { 27 | parentPort.postMessage({ 28 | type: 'done', 29 | idx: workerData.idx, 30 | ...stats, 31 | }); 32 | }, 33 | }); 34 | } 35 | 36 | function start() { 37 | if (isMainThread) { 38 | console.log('子线程处理文件仅支持使用 new workerThreads.Worker 方式调用'); 39 | } else { 40 | startChild(); 41 | } 42 | } 43 | 44 | start(); 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 gflizhiwen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /scripts/verifyCommit.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const msgPath = process.argv[2] || './.git/COMMIT_EDITMSG'; 3 | if (!fs.existsSync(msgPath)) process.exit(); 4 | 5 | const { color } = require('console-log-colors'); 6 | const msg = removeComment(fs.readFileSync(msgPath, 'utf-8').trim()); 7 | const commitRE = 8 | /^(revert: )?(feat|fix|docs|style|refactor|perf|test|workflow|build|ci|chore|types|wip|release|dep|example|Merge)(\(.+\))?: .{1,50}/; 9 | 10 | if (!commitRE.test(msg)) { 11 | console.log(); 12 | console.error( 13 | ` ${color.red(color.bgRed(' ERROR '))} ${color.red( 14 | `invalid commit message format.`, 15 | )}\n\n` + 16 | color.red( 17 | ` Proper commit message format is required for automated changelog generation. Examples:\n\n`, 18 | ) + 19 | ` ${color.green(`feat(bundler-webpack): add 'comments' option`)}\n` + 20 | ` ${color.green(`fix(core): handle events on blur (close #28)`)}\n\n` + 21 | color.red(` See .github/commit-convention.md for more details.\n`), 22 | ); 23 | process.exit(1); 24 | } 25 | 26 | function removeComment(msg) { 27 | return msg.replace(/^#.*[\n\r]*/gm, ''); 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "outDir": "./dist/", 5 | "rootDir": "src", 6 | "moduleResolution": "node", 7 | "module": "commonjs", 8 | "declaration": true, 9 | "sourceMap": false, 10 | "inlineSourceMap": false, 11 | "esModuleInterop": true, 12 | "resolveJsonModule": true, 13 | 14 | // "strict": true, 15 | 16 | /* Strict Type-Checking Options */ 17 | // "noImplicitAny": true, 18 | // "strictNullChecks": true, 19 | // "strictFunctionTypes": true, 20 | // "strictPropertyInitialization": true, 21 | "noImplicitThis": true, 22 | // "alwaysStrict": true, 23 | 24 | /* Additional Checks */ 25 | "noUnusedLocals": true, 26 | "noUnusedParameters": true, 27 | "noImplicitReturns": true, 28 | "noFallthroughCasesInSwitch": true, 29 | 30 | /* Debugging Options */ 31 | "traceResolution": false, 32 | "listEmittedFiles": false, 33 | "listFiles": false, 34 | "pretty": true, 35 | "skipLibCheck": true, 36 | 37 | /* Experimental Options */ 38 | // "experimentalDecorators": true, 39 | // "emitDecoratorMetadata": true, 40 | 41 | "lib": ["esnext"], 42 | "typeRoots": ["types", "node_modules/@types"] 43 | }, 44 | "include": ["src/**/*.ts"], 45 | "exclude": ["node_modules/**"], 46 | "compileOnSave": false 47 | } 48 | -------------------------------------------------------------------------------- /src/dir-rm.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { color } from 'console-log-colors'; 4 | import CONFIG from './config'; 5 | import { showCostTime, logPrint, readSyncByRl } from './utils'; 6 | import type { DfcDirRmOptions } from './type'; 7 | 8 | async function doDirRm(src: string, option: DfcDirRmOptions) { 9 | if (!src) return console.log('请指定要删除的文件或目录路径'); 10 | src = path.resolve(src); 11 | if (!fs.existsSync(src)) return console.log('要删除的文件或目录路径不存在!', color.red(src)); 12 | const srcTip = fs.statSync(src).isFile() ? '文件' : '目录'; 13 | 14 | if (option.slient) CONFIG.slient = true; 15 | if (!option.force) { 16 | const force = await readSyncByRl(`是否删除该${srcTip}(y/)?[${color.red(src)}] `); 17 | if ('y' !== String(force).trim().toLowerCase()) return; 18 | } 19 | const startTime = Date.now(); 20 | 21 | if (typeof fs.rmSync === 'function') fs.rmSync(src, { recursive: true }); 22 | else fs.rmdirSync(src, { recursive: true }); 23 | 24 | logPrint(`$[${showCostTime(startTime)}] ${srcTip}已删除:`, color.green(src)); 25 | return true; 26 | } 27 | 28 | export async function dirRm(option: DfcDirRmOptions) { 29 | if (!Array.isArray(option.src)) return console.log('请指定要删除的文件或目录路径'); 30 | if (option.src.length === 1) return doDirRm(option.src[0], option); 31 | 32 | for (const src of option.src) { 33 | await doDirRm(src, option); 34 | } 35 | 36 | return true; 37 | } 38 | -------------------------------------------------------------------------------- /src/parseConfig.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import { color, log } from 'console-log-colors'; 4 | import globToRegExp from 'glob-to-regexp'; 5 | import CONFIG from './config'; 6 | import { logPrint } from './utils'; 7 | 8 | /** 处理入参信息 */ 9 | export function parseConfig(cfg: typeof CONFIG) { 10 | cfg.src = String(cfg.src || '').trim(); 11 | cfg.dest = String(cfg.dest || '').trim(); 12 | 13 | if (!cfg.src) { 14 | log.red('未指定要复制的源目录!'); 15 | return; 16 | } 17 | 18 | if (!cfg.dest) { 19 | log.red('未指定要复制至的目的目录!'); 20 | return; 21 | } 22 | 23 | cfg.src = path.resolve(cfg.src); 24 | cfg.dest = path.resolve(cfg.dest); 25 | 26 | if (!fs.existsSync(cfg.src)) return console.log(' 源目录不存在,请检查确认:', color.red(cfg.src)); 27 | 28 | if (cfg.dest === cfg.src || cfg.dest.includes(cfg.src + path.sep)) { 29 | log.red('\n源路径不能与目的路径相同或是目的路径的子目录!'); 30 | return; 31 | } 32 | 33 | cfg.minDateTime = cfg.minDateTime ? new Date(cfg.minDateTime).getTime() || 0 : CONFIG.minDateTime; 34 | cfg.mutiThreadMinFiles = Number(cfg.mutiThreadMinFiles) >= 1000 ? Number(cfg.mutiThreadMinFiles) : CONFIG.mutiThreadMinFiles; 35 | cfg.progressInterval = Number(cfg.progressInterval) > 99 ? Number(cfg.progressInterval) : CONFIG.progressInterval; 36 | 37 | Object.assign(CONFIG, cfg); 38 | logPrint('源路径 : ', color.cyan(CONFIG.src), '\n目的路径: ', color.cyan(CONFIG.dest), '\n'); 39 | 40 | // 文件排除规则 41 | if (!CONFIG.exclude) CONFIG.exclude = []; 42 | CONFIG.exclude.forEach((val, i) => { 43 | if (val instanceof RegExp) return; 44 | CONFIG.exclude[i] = globToRegExp(val, { extended: true, flags: 'gi' }); 45 | }); 46 | if (!CONFIG.include) CONFIG.include = []; 47 | CONFIG.include.forEach((val, i) => { 48 | if (val instanceof RegExp) return; 49 | CONFIG.include[i] = globToRegExp(val, { extended: true, flags: 'gi' }); 50 | }); 51 | 52 | // console.debug('CONFIG.exclude', CONFIG.exclude); 53 | if (CONFIG.debug) console.debug('[config][parsed]', CONFIG); 54 | 55 | return CONFIG; 56 | } 57 | -------------------------------------------------------------------------------- /src/type.ts: -------------------------------------------------------------------------------- 1 | export interface DfcConfig { 2 | debug?: boolean; 3 | /** 源目录路径 */ 4 | src: string; 5 | /** 输出目录路径 */ 6 | dest: string; 7 | /** 是否静默模式 */ 8 | slient?: boolean; 9 | /** 是否为 cmd 命令方式调用(dfc --src --dest) */ 10 | iscmd?: boolean; 11 | /** 多线程模式的线程数。小于2表示不启动多线程模式 */ 12 | threads?: number; 13 | /** 启用多线程模式的最小文件数,文件总数低于该值则使用单线程模式(最小值 100,默认为 3000) */ 14 | mutiThreadMinFiles?: number; 15 | /** 文件过滤规则,支持正则和普通的 glob 格式规则 */ 16 | exclude?: any[]; // [/\.pyc$/], 17 | /** 文件包含规则,支持正则和普通的 glob 格式规则 */ 18 | include?: any[]; 19 | /** 文件最小日期,低于该日期的忽略 */ 20 | minDateTime?: number; 21 | /** 文件<名称与大小均相同>已存在是否跳过。为 false 则覆盖它 */ 22 | skipSameFile?: boolean; 23 | /** 多线程模式下,在收集文件信息过程中即启动文件复制(适用于文件数量多、信息收集时间长的场景。默认为 false,在信息收集完毕后才启动) */ 24 | cpDuringStats?: boolean; 25 | /** onProgress 进度回调(进度日志更新)的最小间隔时间(ms),不低于 100ms。默认值 2000 */ 26 | progressInterval?: number; 27 | /** 复制成功后,是否删除源文件。即为 mv 模式 */ 28 | deleteSrc?: boolean; 29 | /** 结束时回调方法 */ 30 | onEnd?: (stats: DfcStats) => void; 31 | /** 发出进度消息时的回调方法 */ 32 | onProgress?: (stats: DfcStats) => void; 33 | } 34 | 35 | export interface FsStatInfo { 36 | isFile?: boolean; 37 | nlink?: number; 38 | isDirectory: boolean; 39 | atime: Date; 40 | mtime: Date; 41 | size: number; 42 | } 43 | 44 | export interface DfcStats { 45 | /** 全部的文件路径 [src, dest] */ 46 | allFilePaths?: { src: string; dest: string; srcStat: FsStatInfo }[]; 47 | /** 全部的目录路径 src?: dest */ 48 | allDirPaths?: { src: string; dest: string; srcStat: FsStatInfo }[]; 49 | /** 文件总数 */ 50 | totalFile?: number; 51 | /** 所有文件的大小之和 */ 52 | totalFileSize?: number; 53 | /** 已处理的文件总数 */ 54 | totalFileHandler?: number; 55 | /** 已复制的文件总数 */ 56 | totalFileNew?: number; 57 | /** 已复制的文件大小之和 */ 58 | totalFileNewSize?: number; 59 | /** 文件夹总数 */ 60 | totalDir?: number; 61 | /** 复制过程中新建的文件夹数 */ 62 | totalDirNew?: number; 63 | } 64 | 65 | export interface PlainObject { 66 | [key: string]: any; 67 | } 68 | 69 | export interface DfcDirRmOptions { 70 | src: string[]; 71 | force?: boolean; 72 | slient?: DfcConfig['slient']; 73 | } 74 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import commander from 'commander'; 4 | import { fastCopy } from './fast-copy'; 5 | import { dirRm } from './dir-rm'; 6 | import type { DfcConfig } from './type'; 7 | 8 | const pkg = require('../package.json'); 9 | const program = commander.program; 10 | 11 | program.version(pkg.version, '-V, --version', '当前版本').helpOption('-h, --help', '查看帮助信息').description(pkg.description); 12 | 13 | const cp = program 14 | .command('cp ') 15 | .description('高效的复制目录') 16 | .option('--debug', '调试模式', false) 17 | .option('-s, --slient', '静默模式', false) 18 | .option('-n, --threads ', '启动多线程的数量。小于 2 表示不启用多线程模式') 19 | .option('-N, --muti-thread-min-files ', '启用多线程的最小文件数,文件总数低于该值则使用单线程模式(最小值 1000,默认为 3000)', '3000') 20 | .option('-e, --exclude ', '文件排除规则。普通的 glob 规则,支持多个参数') 21 | .option('-i, --include ', '文件包含规则。普通的 glob 规则,支持多个参数') 22 | .option('-d, --min-date-time <1970-01-01T00:00:00>', '文件最小日期,低于该日期的文件会被忽略(处理速度更快)') 23 | .option('--no-skip-same-file', '文件<名称与大小均相同>已存在时不跳过(覆盖)。') 24 | .option('-k, --skip-same-file', '文件<名称与大小均相同>已存在时则跳过。', true) 25 | .option('-I, --progress-interval', 'onProgress 进度回调(进度日志更新)的最小间隔时间(ms),不低于 100ms。默认值 2000', '2000') 26 | .option('-c, --cp-during-stats', '多线程模式下,在收集文件信息过程中即开始文件复制(适用于文件数量多信息收集时间长的场景)', false) 27 | .option('-D, --delete-src', '是否删除原文件。即 mv 模式', false) 28 | .action((...args) => { 29 | const config: DfcConfig = { 30 | src: args[0], 31 | dest: args[1], 32 | iscmd: true, 33 | onEnd: () => process.exit(0), 34 | ...cp.opts(), 35 | }; 36 | 37 | Object.keys(config).forEach((key) => { 38 | if (null == config[key]) delete config[key]; 39 | }); 40 | 41 | if (config.debug) console.debug('[cli][config]', config); 42 | 43 | fastCopy(config); 44 | }); 45 | 46 | const rm = program 47 | .command('rm ') 48 | .description('删除一个目录及其子目录') 49 | .option('-f, --force', '强制删除,无需确认(否则删除前需确认)', false) 50 | .option('-s, --slient', '静默模式', false) 51 | .action(() => { 52 | dirRm(Object.assign({ src: rm.args }, rm.opts())); 53 | }); 54 | 55 | program.parse(process.argv); 56 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages 2 | 3 | name: Publish Package to npmjs 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v*.*.*" 9 | # release: 10 | # types: [created] 11 | 12 | jobs: 13 | npm-publish: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | concurrency: 18 | group: ${{ github.workflow }}-${{ github.ref }} 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: 'recursive' 24 | 25 | - name: Install Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 20 29 | registry-url: https://registry.npmjs.com 30 | 31 | - uses: pnpm/action-setup@v4 32 | name: Install pnpm 33 | id: pnpm-install 34 | with: 35 | version: 9 36 | run_install: false 37 | 38 | - name: Get pnpm store directory 39 | id: pnpm-cache 40 | shell: bash 41 | run: | 42 | echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT 43 | 44 | - uses: actions/cache@v3 45 | name: Setup pnpm cache 46 | with: 47 | path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} 48 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 49 | restore-keys: | 50 | ${{ runner.os }}-pnpm-store- 51 | 52 | - name: Install dependencies 53 | run: pnpm install --no-frozen-lockfile # --ignore-scripts 54 | 55 | - name: Build and npm-publish 56 | run: npm run build 57 | 58 | - run: npm publish 59 | env: 60 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 61 | 62 | # run: npm run publish:github # --registry=https://npm.pkg.github.com 63 | # env: 64 | # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} 65 | 66 | - name: GitHub Pages action 67 | uses: peaceiris/actions-gh-pages@v3 68 | with: 69 | github_token: ${{ secrets.GITHUB_TOKEN }} 70 | publish_dir: ./docs 71 | 72 | - name: Github Release 73 | uses: softprops/action-gh-release@v1 74 | if: startsWith(github.ref, 'refs/tags/') 75 | with: 76 | draft: false 77 | prerelease: false 78 | # tag_name: ${{ github.ref }} 79 | # name: Release ${{ github.ref }} 80 | # body: TODO New Release. 81 | # files: | 82 | # ${{ secrets.ReleaseZipName }}.zip 83 | # LICENSE 84 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.6.1](https://github.com/lzwme/dir-fast-copy/compare/v1.6.0...v1.6.1) (2025-03-25) 6 | 7 | ## [1.6.0](https://github.com/lzwme/dir-fast-copy/compare/v1.5.0...v1.6.0) (2025-03-17) 8 | 9 | 10 | ### Features 11 | 12 | * 新增 include 参数,支持指定文件包含规则 ([eeee0fa](https://github.com/lzwme/dir-fast-copy/commit/eeee0faa3799424426709352dcbb0a66570acf56)) 13 | 14 | ## [1.5.0](https://github.com/lzwme/dir-fast-copy/compare/v1.4.1...v1.5.0) (2023-12-14) 15 | 16 | 17 | ### Features 18 | 19 | * 新增 deleteSrc 参数,可选择复制成功后是否删除源文件(即mv模式) ([0531e9a](https://github.com/lzwme/dir-fast-copy/commit/0531e9a7eb25658ff5fb174cde7b39b93c0fcf37)) 20 | 21 | ### [1.4.1](https://github.com/lzwme/dir-fast-copy/compare/v1.4.0...v1.4.1) (2022-11-02) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * 更正 cpFile 等待逻辑 ([a9742a9](https://github.com/lzwme/dir-fast-copy/commit/a9742a9070b88e7e2b1b27b03fca9abe986ad840)) 27 | * 修复 exclude 参数不生效的问题 ([a0b49af](https://github.com/lzwme/dir-fast-copy/commit/a0b49afec063e82a5587c98c7481cd5a357b233b)) 28 | 29 | ## 1.4.0 (2022-06-22) 30 | 31 | 32 | ### Features 33 | 34 | * 新增 cpDuringStats 参数,用于指定多线程模式下在收集文件信息过程中是否进行文件复制(默认所有文件信息收集完毕才开始) ([8c5d9cc](https://github.com/lzwme/dir-fast-copy/commit/8c5d9cc4ea0c592222f9a1c2d072e7151d3c3a88)) 35 | * 增加显示复制文件的大小信息;更新依赖 ([90e342e](https://github.com/lzwme/dir-fast-copy/commit/90e342ea34483da044846e776361199145e4ca50)) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * 修正单词简写错误 desc -> dest (close [#1](https://github.com/lzwme/dir-fast-copy/issues/1)) ([715c8b8](https://github.com/lzwme/dir-fast-copy/commit/715c8b8d7b69fef10196da55419e7f8628b38780)) 41 | * console-log-colors 依赖应放到 dependencies 中 ([5788e00](https://github.com/lzwme/dir-fast-copy/commit/5788e0046399b58af1f3778227faf0ab7d65ce97)) 42 | 43 | ## 1.3.0 (2022-04-20) 44 | 45 | 46 | ### Features 47 | 48 | * 新增 cpDuringStats 参数,用于指定多线程模式下在收集文件信息过程中是否进行文件复制(默认所有文件信息收集完毕才开始) ([8c5d9cc](https://github.com/lzwme/dir-fast-copy/commit/8c5d9cc4ea0c592222f9a1c2d072e7151d3c3a88)) 49 | * 增加显示复制文件的大小信息;更新依赖 ([90e342e](https://github.com/lzwme/dir-fast-copy/commit/90e342ea34483da044846e776361199145e4ca50)) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * console-log-colors 依赖应放到 dependencies 中 ([5788e00](https://github.com/lzwme/dir-fast-copy/commit/5788e0046399b58af1f3778227faf0ab7d65ce97)) 55 | 56 | ## 1.2.0 (2020-09-23) 57 | 58 | 59 | ### Features 60 | 61 | * 新增 cpDuringStats 参数,用于指定多线程模式下在收集文件信息过程中是否进行文件复制(默认所有文件信息收集完毕才开始) ([8c5d9cc](https://github.com/lzwme/dir-fast-copy/commit/8c5d9cc4ea0c592222f9a1c2d072e7151d3c3a88)) 62 | 63 | ### Bug Fixes 64 | 65 | * console-log-colors 依赖应放到 dependencies 中 ([5788e00](https://github.com/lzwme/dir-fast-copy/commit/5788e0046399b58af1f3778227faf0ab7d65ce97)) 66 | 67 | ### 1.0.0 (2020-09-21) 68 | 69 | * init 70 | 71 | ### Features 72 | 73 | * TODO -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@lzwme/dir-fast-copy", 3 | "version": "1.6.1", 4 | "description": "nodejs 实现的文件夹快速复制工具。适用于对存在海量小文件的目录进行选择性复制的需求场景。", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "bin": { 8 | "dfc": "bin/dfc.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/lzwme/dir-fast-copy.git" 13 | }, 14 | "license": "MIT", 15 | "keywords": [ 16 | "fast", 17 | "copy", 18 | "cp", 19 | "文件复制" 20 | ], 21 | "author": { 22 | "name": "lzwme", 23 | "url": "https://lzw.me" 24 | }, 25 | "maintainers": [ 26 | { 27 | "name": "renxia", 28 | "url": "https://lzw.me" 29 | } 30 | ], 31 | "scripts": { 32 | "dev": "tsc -w", 33 | "build": "run-s clean && run-p build:*", 34 | "build:dist": "tsc -p tsconfig.json", 35 | "fix": "run-s fix:*", 36 | "fix:prettier": "prettier \"src/**/*.ts\" --write", 37 | "test": "run-s build test:*", 38 | "test:lint": "prettier \"src/**/*[^.spec].ts\" --list-different", 39 | "test:unit": "nyc --silent ava", 40 | "watch": "run-s clean build:dist && run-p \"build:dist -- -w\" \"test:unit -- --watch\"", 41 | "watch:src": "run-s clean build:dist && run-p \"build:dist -- -w\"", 42 | "cov": "run-s build test:unit cov:html && open-cli coverage/index.html", 43 | "cov:html": "nyc report --reporter=html", 44 | "cov:send": "nyc report --reporter=lcov && codecov", 45 | "cov:summary": "nyc report --reporter=text-summary", 46 | "cov:check": "nyc report && nyc check-coverage --lines 40 --functions 30 --branches 30", 47 | "version": "standard-version", 48 | "reset": "git clean -dfx && git reset --hard && pnpm install", 49 | "clean": "trash dist cjs esm", 50 | "preinstall": "husky install", 51 | "prepare-release": "run-s test cov:check version" 52 | }, 53 | "engines": { 54 | "node": ">=12" 55 | }, 56 | "publishConfig": { 57 | "access": "public", 58 | "registry": "https://registry.npmjs.com" 59 | }, 60 | "devDependencies": { 61 | "@istanbuljs/nyc-config-typescript": "^1.0.2", 62 | "@types/node": "^22", 63 | "ava": "^6.2.0", 64 | "cz-conventional-changelog": "^3.3.0", 65 | "husky": "^9.1.7", 66 | "lint-staged": "^15.5.0", 67 | "npm-run-all": "^4.1.5", 68 | "nyc": "^17.1.0", 69 | "open-cli": "^8.0.0", 70 | "prettier": "^3.5.3", 71 | "standard-version": "^9.5.0", 72 | "trash-cli": "^6.0.0", 73 | "ts-node": "^10.9.2", 74 | "typescript": "^5.8.2" 75 | }, 76 | "dependencies": { 77 | "commander": "^13.1.0", 78 | "console-log-colors": "^0.5.0", 79 | "glob-to-regexp": "^0.4.1" 80 | }, 81 | "ava": { 82 | "failFast": true, 83 | "files": [ 84 | "dist/**/*.spec.js" 85 | ], 86 | "watchMode": { 87 | "ignoreChanges": [ 88 | "dist/**/*.js" 89 | ] 90 | } 91 | }, 92 | "nyc": { 93 | "extends": "@istanbuljs/nyc-config-typescript", 94 | "exclude": [ 95 | "**/*.spec.js" 96 | ] 97 | }, 98 | "prettier": { 99 | "singleQuote": true 100 | }, 101 | "lint-staged": { 102 | "*.ts": [ 103 | "prettier --parser typescript --write --cache" 104 | ], 105 | "*.json": [ 106 | "prettier --parser json --write" 107 | ] 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code Climate](https://lzw.me/images/logo.png)](https://lzw.me) 2 | [![@lzwme/dir-fast-copy](https://nodei.co/npm/@lzwme/dir-fast-copy.png)][download-url] 3 | 4 | @lzwme/dir-fast-copy 5 | ======== 6 | 7 | [![NPM version][npm-image]][npm-url] 8 | [![node version][node-image]][node-url] 9 | [![npm download][download-image]][download-url] 10 | [![GitHub issues][issues-img]][issues-url] 11 | [![GitHub forks][forks-img]][forks-url] 12 | [![GitHub stars][stars-img]][stars-url] 13 | 14 | [stars-img]: https://img.shields.io/github/stars/lzwme/dir-fast-copy.svg 15 | [stars-url]: https://github.com/lzwme/dir-fast-copy/stargazers 16 | [forks-img]: https://img.shields.io/github/forks/lzwme/dir-fast-copy.svg 17 | [forks-url]: https://github.com/lzwme/dir-fast-copy/network 18 | [issues-img]: https://img.shields.io/github/issues/lzwme/dir-fast-copy.svg 19 | [issues-url]: https://github.com/lzwme/dir-fast-copy/issues 20 | [npm-image]: https://img.shields.io/npm/v/@lzwme/dir-fast-copy.svg?style=flat-square 21 | [npm-url]: https://npmjs.org/package/@lzwme/dir-fast-copy 22 | [node-image]: https://img.shields.io/badge/node.js-%3E=_12-green.svg?style=flat-square 23 | [node-url]: https://nodejs.org/download/ 24 | [download-image]: https://img.shields.io/npm/dm/@lzwme/dir-fast-copy.svg?style=flat-square 25 | [download-url]: https://npmjs.org/package/@lzwme/dir-fast-copy 26 | 27 | 28 | nodejs 实现的文件夹快速复制工具。适用于对存在海量小文件的目录进行选择性复制的需求场景。 29 | 30 | 对于前端开发来说,在 windows 系统下对某些庞大的目录进行复制时,系统自带的复制功能可能令人十分痛苦。如果你遇到此类需求,dfc 小工具或许可以帮助到你。 31 | 32 | ![](docs/dfc-cp-thread-8.png) 33 | 34 | ![](docs/dfc-cp-2.png) 35 | 36 | ## 功能特点 37 | 38 | - 针对海量小文件,多线程复制速度快 39 | - 自动跳过已存在且大小相同的文件 40 | - 支持模糊过滤,忽略部分文件 41 | - 支持按文件修改时间过滤,只复制新产生的文件 42 | - more... 43 | 44 | ## 安装与使用 45 | 46 | ### 全部安装 47 | 48 | ```bash 49 | npm i -g @lzwme/dir-fast-copy 50 | ``` 51 | 52 | ### 使用 53 | 54 | ```bash 55 | dfc --help 56 | 57 | Usage: dfc [options] [command] 58 | 59 | Commands: 60 | cp [options] 高效的复制目录 61 | rm [options] 删除一个目录及其子目录 62 | ``` 63 | 64 | **目录复制:** 65 | 66 | ```bash 67 | dfc cp --help 68 | 69 | Usage: dfc cp [options] 70 | 71 | 高效的复制目录 72 | 73 | Options: 74 | --debug 调试模式 (default: false) 75 | -s, --slient 静默模式 (default: false) 76 | --threads 启动多线程的数量。小于 2 表示不启用多线程模式 77 | --muti-thread-min-files 启用多线程的最小文件数,文件总数低于该值则使用单线程模式(最小值 1000,默认为 3000) 78 | --exclude 文件排除规则。普通的 glob 规则,支持多个参数 79 | --min-date-time <1970-01-01T00:00:00> 文件最小日期,低于该日期的文件会被忽略(处理速度更快) 80 | --no-skip-same-file 文件<名称与大小均相同>已存在时不跳过(覆盖)。 81 | --skip-same-file 文件<名称与大小均相同>已存在时则跳过。 (default: true) 82 | --progress-interval onProgress 进度回调(进度日志更新)的最小间隔时间(ms),不低于 100ms。默认值 2000 83 | --cp-during-stats 多线程模式下,在收集文件信息过程中即开始文件复制(适用于文件数量多信息收集时间长的场景) (default: false) 84 | -h, --help 查看帮助信息 85 | ``` 86 | 87 | 示例: 88 | ```bash 89 | dfc cp ./src ./dest 90 | 91 | # 使用 8 子线程复制 92 | dfc cp ./src ./dest --threads 8 93 | 94 | # 多线程复制,在收集目录信息时即进行部分复制 95 | dfc cp ./src ./dest --cp-during-stats 96 | 97 | # 复制 src 目录至 dest,排除 node_modules 和 dist 目录,排除 .pyc 和 .obj 类型的文件 98 | dfc cp ./src ./dest --exclude /node_modules/** dist/** *.{pyc, .obj} 99 | 100 | # 只复制 2020-09-22 00:00:00 之后改动或创建的文件 101 | dfc cp ./src ./dest --min-date-time 2020-09-22 102 | 103 | # 强制复制所有文件 104 | dfc cp ./src ./dest --no-skip-same-file 105 | ``` 106 | 107 | **目录删除:** 108 | 109 | ```bash 110 | dfc rm --help 111 | 112 | Usage: dfc rm [options] 113 | 114 | 删除一个目录及其子目录 115 | 116 | Options: 117 | -f, --force 强制删除,无需确认(否则删除前需确认) (default: false) 118 | -h, --help 查看帮助信息 119 | ``` 120 | 121 | 示例: 122 | 123 | ```bash 124 | dfc rm ./lzwme-test1 125 | dfc rm -f ./lzwme-test2 126 | dfc rm -f ./lzwme-test3.txt 127 | ``` 128 | 129 | ## API 调用方式 130 | 131 | ### es module 132 | 133 | ```js 134 | import { fastCopy } from '@lzwme/dir-fast-copy'; 135 | 136 | const config = { 137 | src: './test-src', 138 | dest: './test-dest', 139 | onProgress: stats => console.log('onProgress:', onProgress), 140 | onEnd: stats => console.log('onEnd:', onProgress), 141 | }; 142 | 143 | fastCopy(config).then(stats => { 144 | if (stats) console.log('done:', stats); 145 | }); 146 | ``` 147 | 148 | ### Options 149 | 150 | ```js 151 | /** 默认配置 */ 152 | const config = { 153 | /** 是否静默模式 */ 154 | slient: false, 155 | /** 是否为 cmd 命令方式调用(dfc --src --dest) */ 156 | iscmd: false, 157 | /** 源目录路径 */ 158 | src: '', 159 | /** 输出目录路径 */ 160 | dest: '', 161 | /** 是否尝试启用多线程模式 */ 162 | enableThreads: true, 163 | /** 启用线程的最小文件数,文件总数低于该值则不启用多线程模式 */ 164 | enableThreadsMinCount: 500, 165 | /** 文件过滤规则 */ 166 | filterRegList: [/\.pyc$/, /\/src\/out\//], 167 | /** 文件最小日期,低于该日期的忽略 */ 168 | minDatetime: new Date('2000-09-15T00:00:00').getTime(), 169 | /** 目的目录相同文件已存在是否跳过。为 false 则覆盖它 */ 170 | isSkipSameFile: true, 171 | /** 结束时回调方法 */ 172 | onEnd: null, 173 | /** 发出进度消息时的回调方法 */ 174 | onProgress: null, 175 | }; 176 | ``` 177 | 178 | ## License 179 | 180 | `@lzwme/dir-fast-copy` is released under the MIT license. 181 | 182 | 该插件由[志文工作室](https://lzw.me)开发和维护。 183 | -------------------------------------------------------------------------------- /benchmark/test-fs.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import * as path from 'path'; 3 | import { calcTimeCost, rootDir } from './utils'; 4 | 5 | export function getAllFilesSync(_srcDir, _destDir = '', onProgress?) { 6 | const stats = { 7 | totalFile: 0, 8 | totalDir: 0, 9 | allDirPaths: [] as [string, string][], 10 | allFilePaths: [] as [string, string][], 11 | }; 12 | const handler = (srcDir, destDir = '') => { 13 | const filelist = fs.readdirSync(srcDir, { encoding: 'utf8' }); 14 | if (onProgress && stats.totalFile && 0 === stats.totalFile % 500) { 15 | onProgress(Object.assign({}, stats)); 16 | } 17 | filelist.forEach((filename) => { 18 | if (!filename) 19 | return; 20 | const srcPath = path.resolve(srcDir, filename); 21 | const destPath = destDir ? path.resolve(destDir, filename) : ''; 22 | if (!fs.existsSync(srcPath)) return; 23 | if (fs.statSync(srcPath).isDirectory()) { 24 | stats.totalDir++; 25 | stats.allDirPaths.push([srcPath, destPath]); 26 | return handler(srcPath, destPath); 27 | } 28 | else { 29 | stats.totalFile++; 30 | stats.allFilePaths.push([srcPath, destPath]); 31 | } 32 | }); 33 | }; 34 | handler(_srcDir, _destDir); 35 | console.log(stats.totalFile); 36 | return stats; 37 | } 38 | 39 | export async function getAllFiles(_srcDir, _destDir = '', onProgress?) { 40 | const stats = { 41 | totalFile: 0, 42 | totalDir: 0, 43 | allDirPaths: [] as [string, string][], 44 | allFilePaths: [] as [string, string][], 45 | }; 46 | 47 | const handler = async (srcDir, destDir = '') => { 48 | const filelist = await fs.promises.readdir(srcDir, { encoding: 'utf8' }); 49 | if (onProgress && stats.totalFile && 0 === stats.totalFile % 500) { 50 | onProgress(Object.assign({}, stats)); 51 | } 52 | 53 | const list = filelist.map( async (filename) => { 54 | if (!filename) return; 55 | 56 | const srcPath = path.resolve(srcDir, filename); 57 | const destPath = destDir ? path.resolve(destDir, filename) : ''; 58 | if (!fs.existsSync(srcPath)) return; 59 | 60 | if ((await fs.promises.stat(srcPath)).isDirectory()) { 61 | stats.totalDir++; 62 | stats.allDirPaths.push([srcPath, destPath]); 63 | return handler(srcPath, destPath); 64 | } else { 65 | stats.totalFile++; 66 | stats.allFilePaths.push([srcPath, destPath]); 67 | } 68 | }); 69 | 70 | await Promise.all(list); 71 | } 72 | await handler(_srcDir, _destDir); 73 | console.log(stats.totalFile); 74 | return stats; 75 | } 76 | 77 | async function startGetFiles() { 78 | const src = path.resolve(rootDir, `node_modules/.pnpm`); 79 | 80 | await calcTimeCost(() => getAllFiles(src, 'tmp/test-2'), 'getAllFiles'); 81 | await calcTimeCost(() => getAllFilesSync(src, 'tmp/test-1'), 'getAllFilesSync'); 82 | } 83 | 84 | // --------------------- fs-copy ---------------- 85 | 86 | function fsCopy(src: string, dest = 'tmp/fs-cp-ts') { 87 | return new Promise(rs => { 88 | fs.cp(src, dest, { recursive: true }, () => rs(true)); 89 | }); 90 | } 91 | 92 | function fsCopySync(src: string, dest = 'tmp/fs-cpSync-ts') { 93 | fs.cpSync(src, dest, { recursive: true }); 94 | } 95 | 96 | function dirCopySync(src: string, dest = 'tmp/dirCopySync') { 97 | return dirCopyRecursive(src, dest); 98 | } 99 | 100 | async function dirCopy(src: string, dest = 'tmp/dirCopy') { 101 | const stats = { 102 | totalFile: 0, // 文件总数 103 | totalFileSize: 0, 104 | totalFileHandler: 0, // 已处理的文件数 105 | totalFileNew: 0, // 复制了多少个文件 106 | totalFileNewSize: 0, 107 | totalDirNew: 0, // 创建了多少个目录 108 | totalDir: 0, 109 | }; 110 | 111 | const handler = async (srcDir: string, destDir: string) => { 112 | if (!fs.existsSync(destDir)) { 113 | cpDir(srcDir, destDir); 114 | stats.totalDirNew++; 115 | } 116 | 117 | const filelist = await fs.promises.readdir(srcDir, { encoding: 'utf8' }); 118 | let srcPath = ''; 119 | let destPath = ''; 120 | 121 | for (const filename of filelist) { 122 | if (!filename || filename === '..') continue; 123 | 124 | srcPath = path.resolve(srcDir, filename); 125 | destPath = path.resolve(destDir, filename); 126 | 127 | const srcStat = await fs.promises.stat(srcPath); 128 | 129 | if (srcStat.isFile()) { 130 | stats.totalFileHandler++; 131 | stats.totalFile = stats.totalFileHandler; 132 | stats.totalFileSize += srcStat.size; 133 | } else { 134 | stats.totalDir++; 135 | } 136 | 137 | if (srcStat.isDirectory()) { 138 | await handler(srcPath, destPath); 139 | // 移除空的文件夹 140 | if (!(await fs.promises.readdir(destPath)).length) { 141 | await fs.promises.rmdir(destPath); 142 | stats.totalDirNew--; 143 | } 144 | continue; 145 | } 146 | 147 | cpFile(srcPath, destPath, srcStat); 148 | stats.totalFileNew++; 149 | stats.totalFileNewSize += srcStat.size; 150 | } 151 | }; 152 | 153 | await handler(src, dest); 154 | stats.totalFile = stats.totalFileHandler; 155 | console.log('stats.totalFile', stats.totalFile) 156 | return stats; 157 | } 158 | 159 | /** 单线程模式,执行目录复制(递归) */ 160 | export function dirCopyRecursive(src: string, dest: string, onProgress?: (stats) => void) { 161 | const stats = { 162 | totalFile: 0, // 文件总数 163 | totalFileSize: 0, 164 | totalFileHandler: 0, // 已处理的文件数 165 | totalFileNew: 0, // 复制了多少个文件 166 | totalFileNewSize: 0, 167 | totalDirNew: 0, // 创建了多少个目录 168 | totalDir: 0, 169 | }; 170 | 171 | const handler = (srcDir: string, destDir: string) => { 172 | if (!fs.existsSync(destDir)) { 173 | cpDir(srcDir, destDir); 174 | stats.totalDirNew++; 175 | } 176 | 177 | const filelist = fs.readdirSync(srcDir, { encoding: 'utf8' }); 178 | let srcPath = ''; 179 | let destPath = ''; 180 | 181 | filelist.forEach((filename) => { 182 | if (!filename || filename === '..') return; 183 | 184 | onProgress && onProgress(stats); 185 | srcPath = path.resolve(srcDir, filename); 186 | destPath = path.resolve(destDir, filename); 187 | 188 | const srcStat = fs.statSync(srcPath); 189 | 190 | if (srcStat.isFile()) { 191 | stats.totalFileHandler++; 192 | stats.totalFile = stats.totalFileHandler; 193 | stats.totalFileSize += srcStat.size; 194 | } else { 195 | stats.totalDir++; 196 | } 197 | 198 | if (srcStat.isDirectory()) { 199 | handler(srcPath, destPath); 200 | // 移除空的文件夹 201 | if (!fs.readdirSync(destPath).length) { 202 | fs.rmdirSync(destPath); 203 | stats.totalDirNew--; 204 | } 205 | return; 206 | } 207 | 208 | cpFile(srcPath, destPath, srcStat); 209 | stats.totalFileNew++; 210 | stats.totalFileNewSize += srcStat.size; 211 | }); 212 | }; 213 | 214 | handler(src, dest); 215 | stats.totalFile = stats.totalFileHandler; 216 | console.log('stats.totalFile', stats.totalFile) 217 | return stats; 218 | } 219 | 220 | /** 复制一个文件(不作任何检查以保证速度) */ 221 | export function cpFile(srcPath, destPath, srcStat?: fs.Stats) { 222 | try { 223 | if (!srcStat) srcStat = fs.statSync(srcPath); 224 | // fs.writeFileSync(destPath, fs.readFileSync(srcPath)); 225 | fs.createReadStream(srcPath).pipe(fs.createWriteStream(destPath)); 226 | fs.utimesSync(destPath, srcStat.atime, srcStat.mtime); 227 | // totalFileNew++; 228 | } catch (err) { 229 | console.log(`文件复制失败:\nsrc: ${srcPath}\ndest: ${destPath}\n`, err); 230 | } 231 | } 232 | 233 | /** 复制一个目录(不作任何检查以保证速度) */ 234 | export function cpDir(srcDir, destDir, srcStat?: fs.Stats) { 235 | try { 236 | if (!srcStat) srcStat = fs.statSync(srcDir); 237 | fs.mkdirSync(destDir, { recursive: true }); 238 | fs.utimesSync(destDir, srcStat.atime, srcStat.mtime); 239 | } catch (err) { 240 | console.log(`文件复制失败:\nsrc: ${srcDir}\ndest: ${destDir}\n`, err); 241 | } 242 | } 243 | 244 | 245 | async function startCopy() { 246 | const src = path.resolve(rootDir, `node_modules\\.pnpm`); 247 | 248 | await calcTimeCost(() => dirCopySync(src), 'dirCopySync'); 249 | await calcTimeCost(() => dirCopy(src), 'dirCopy'); 250 | await calcTimeCost(() => fsCopySync(src), 'fsCopySync'); 251 | await calcTimeCost(() => fsCopy(src), 'fsCopy'); 252 | } 253 | 254 | startGetFiles(); 255 | startCopy(); 256 | -------------------------------------------------------------------------------- /src/fast-copy.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * @Author: lzw 3 | * @Date: 2020-09-18 09:52:53 4 | * @LastEditors: renxia 5 | * @LastEditTime: 2023-12-14 14:58:57 6 | * @Description: 对指定文件夹内的文件进行复制,只复制指定日期之后创建的文件 7 | */ 8 | 9 | import * as workerThreads from 'worker_threads'; 10 | import * as fs from 'fs'; 11 | import * as path from 'path'; 12 | import { color, log } from 'console-log-colors'; 13 | import { CONFIG } from './config'; 14 | import { 15 | cpFile, 16 | cpDir, 17 | fileCopy, 18 | showCostTime, 19 | dirCopyRecursive, 20 | logInline, 21 | logPrint, 22 | getAllFiles, 23 | formatFileSize, 24 | toFSStatInfo, 25 | } from './utils'; 26 | import { DfcConfig, DfcStats } from './type'; 27 | import { parseConfig } from './parseConfig'; 28 | 29 | /** 简单处理单文件的复制 */ 30 | async function cpSingleFile(srcFilePath, destFilePath) { 31 | const startTime = Date.now(); 32 | const srcStat = fs.statSync(srcFilePath); 33 | logPrint('单文件复制'); 34 | if (fs.existsSync(destFilePath) && srcStat.isFile()) { 35 | logPrint('目的文件已存在,将被源文件替换'); 36 | } 37 | await cpFile(srcFilePath, destFilePath, toFSStatInfo(srcStat)); 38 | logPrint(`复制完成,耗时 ${color.green((Date.now() - startTime) / 1000)} 秒`); 39 | return true; 40 | } 41 | /** 多线程复制模式 */ 42 | function mutiThreadCopy( 43 | allFilePathList: any[][], 44 | opts: { 45 | startTime?: number; 46 | onStart?: (threadNum: number) => void; 47 | onProgress?: DfcConfig['onProgress']; 48 | onEnd?: DfcConfig['onEnd']; 49 | } = {}, 50 | ) { 51 | const stats: DfcStats = { 52 | totalFile: allFilePathList.length, 53 | }; 54 | /** 当前运行的子线程,当为 0 时表示全部执行结束 */ 55 | let threadRuningNum = CONFIG.threads; 56 | const sepCount = Math.ceil(allFilePathList.length / CONFIG.threads); 57 | /** 各子线程的统计信息,以 idx 为 key */ 58 | const threadsStats = {}; 59 | /** 最近一次执行 onProgress 的时间 */ 60 | let preNotifyProgressTime = 0; 61 | const workerOnData = (worker: workerThreads.Worker, data) => { 62 | // logPrint(`子线程${idx}发来消息:`, data); 63 | threadsStats[data.idx] = data; 64 | 65 | if (data.type === 'progress') { 66 | if (Date.now() - preNotifyProgressTime < CONFIG.progressInterval) return; 67 | preNotifyProgressTime = Date.now(); 68 | } 69 | 70 | stats.totalFileHandler = 0; 71 | stats.totalFileNew = 0; 72 | stats.totalFileNewSize = 0; 73 | stats.totalFileSize = 0; 74 | stats.totalDirNew = 0; 75 | Object.keys(threadsStats).forEach((key) => { 76 | const item = threadsStats[key]; 77 | stats.totalFileHandler += item.totalFileHandler; 78 | stats.totalFileNew += item.totalFileNew; 79 | stats.totalFileNewSize += item.totalFileNewSize; 80 | stats.totalFileSize += item.totalFileSize; 81 | stats.totalDirNew += item.totalDirNew; 82 | }); 83 | 84 | if (data.type === 'progress' && opts.onProgress) process.nextTick(() => opts.onProgress(stats)); 85 | // if (stats.totalFileHandler && 0 === stats.totalFileHandler % 1000) {} 86 | 87 | if (data.type === 'done') { 88 | threadRuningNum--; 89 | worker.terminate(); 90 | if (!threadRuningNum) { 91 | if (opts.onEnd) process.nextTick(() => opts.onEnd(stats)); 92 | } 93 | } 94 | }; 95 | 96 | if (opts.onStart) opts.onStart(CONFIG.threads); 97 | 98 | const childCfg = { ...CONFIG }; 99 | Object.keys(childCfg).forEach((key) => { 100 | // postMessage 不能传递函数类型 101 | if (typeof childCfg[key] === 'function') delete childCfg[key]; 102 | }); 103 | 104 | for (let idx = 0; idx < CONFIG.threads; idx++) { 105 | const workerFile = path.resolve(__dirname, './worker.js'); 106 | const workerData = { 107 | idx, 108 | sepCount, 109 | config: childCfg, 110 | startTime: opts.startTime || Date.now(), 111 | filePathList: allFilePathList.slice(idx * sepCount, (idx + 1) * sepCount), 112 | }; 113 | const worker = new workerThreads.Worker(workerFile, { workerData }); 114 | 115 | logPrint(`启动子线程 ${idx},待处理文件数为:`, color.yellow(workerData.filePathList.length)); 116 | worker.on('message', workerOnData.bind(globalThis, worker)); 117 | } 118 | } 119 | 120 | async function startMain(_config: typeof CONFIG): Promise { 121 | const STATS: DfcStats = { 122 | allFilePaths: [], 123 | allDirPaths: [], 124 | totalFile: 0, 125 | totalFileSize: 0, 126 | totalFileHandler: 0, 127 | totalFileNew: 0, 128 | totalFileNewSize: 0, 129 | totalDir: 0, 130 | totalDirNew: 0, 131 | }; 132 | /** 开始时间 */ 133 | const startTime = Date.now(); 134 | /** 打印进度信息 */ 135 | const logProgress = (showPercent = true, s = STATS) => { 136 | if (CONFIG.slient) return; 137 | const percent = showPercent ? `[${((100 * s.totalFileHandler) / s.totalFile).toFixed(2)}%]` : ''; 138 | logInline( 139 | `[${showCostTime(startTime)}] ${percent} 已处理了${color.yellow(s.totalFileHandler)} 个文件,其中复制了 ${color.magenta( 140 | s.totalFileNew, 141 | )} 个文件${s.totalFileNewSize ? `(${color.magentaBright(formatFileSize(s.totalFileNewSize))})` : ''}`, 142 | ); 143 | }; 144 | 145 | const cfg = parseConfig(_config); 146 | if (!cfg) return STATS; 147 | 148 | // 单文件复制 149 | if (fs.statSync(cfg.src).isFile()) { 150 | await cpSingleFile(cfg.src, cfg.dest); 151 | STATS.totalFileNew = STATS.totalFile = 1; 152 | return STATS; 153 | } 154 | 155 | return new Promise(async (resolve) => { 156 | if (!fs.existsSync(cfg.dest)) { 157 | cpDir(cfg.src, cfg.dest); 158 | STATS.totalDirNew++; 159 | } 160 | 161 | /** 执行完成后回调方法 */ 162 | const onEnd = () => { 163 | if (CONFIG.deleteSrc === true) { 164 | STATS.allDirPaths.forEach((dirInfo) => { 165 | if (fs.existsSync(dirInfo.src) && fs.readdirSync(dirInfo.src).length === 0) fs.rmdirSync(dirInfo.src); 166 | }); 167 | } 168 | 169 | logInline( 170 | `\n处理完成,总耗时 ${color.green((Date.now() - startTime) / 1000)} 秒!共处理了 ${color.yellow(STATS.totalFile)} 个文件${ 171 | STATS.totalFileSize ? `(${color.yellowBright(formatFileSize(STATS.totalFileSize))})` : '' 172 | },包含于 ${color.cyan(STATS.totalDir)} 个文件夹中。其中复制了 ${color.magenta(STATS.totalFileNew)} 个文件${ 173 | STATS.totalFileNewSize ? `(${color.magentaBright(formatFileSize(STATS.totalFileNewSize))})` : '' 174 | }\n`, 175 | ); 176 | // 执行了 ${color.cyan(STATS.totalDirNew)} 次文件夹创建命令 // 由于多线程模式下用了递归创建参数,该值不准确 177 | 178 | if (cfg.onEnd) cfg.onEnd(STATS); 179 | resolve(STATS); 180 | }; 181 | 182 | if (+CONFIG.threads < 2) { 183 | logPrint(color.cyan('单线程模式')); 184 | /** 最近一次执行 onProgress 的时间 */ 185 | let preNotifyProgressTime = 0; 186 | const stats = dirCopyRecursive(cfg.src, cfg.dest, (s) => { 187 | if (Date.now() - preNotifyProgressTime < CONFIG.progressInterval) return; 188 | preNotifyProgressTime = Date.now(); 189 | Object.assign(STATS, s); 190 | logProgress(false); 191 | }); 192 | Object.assign(STATS, stats); 193 | onEnd(); 194 | } else { 195 | logPrint(color.cyan('开始收集源目录内文件信息')); 196 | /** 待复制的文件列表 */ 197 | let allFileListTodo = []; 198 | /** 已发送给子线程处理的文件数 */ 199 | let sendedToCpFileNum = 0; 200 | /** 子线程是否已处理完毕 */ 201 | let isDone = true; 202 | const stats = await getAllFiles(cfg.src, cfg.dest, (s) => { 203 | logInline(`[${showCostTime(startTime)}] 已发现目录数:${color.cyan(s.totalDir)} 个,包含文件 ${color.cyanBright(s.totalFile)} 个`); 204 | 205 | // TODO: 可以在获取到文件后立即执行多线程复制 206 | if (CONFIG.cpDuringStats && isDone && s.totalFile > CONFIG.mutiThreadMinFiles) { 207 | allFileListTodo = s.allFilePaths.slice(sendedToCpFileNum); 208 | 209 | if (allFileListTodo.length > CONFIG.mutiThreadMinFiles) { 210 | isDone = false; 211 | sendedToCpFileNum = s.totalFile; 212 | mutiThreadCopy(allFileListTodo, { 213 | startTime, 214 | onStart: () => { 215 | logPrint(color.gray('\n\n 数据收集过程中启动线程复制,本次处理文件数:'), allFileListTodo.length, '\n'); 216 | }, 217 | // onProgress: (s) => logProgress(true, s), 218 | onEnd: (s) => { 219 | // 只记录复制了的文件和文件夹,因为他们还会在后面被处理 220 | ['totalDirNew', 'totalFileNew', 'totalFileSize', 'totalFileNewSize'].forEach((key) => { 221 | if (s[key]) STATS[key] += s[key]; 222 | }); 223 | logPrint(color.gray(`\n 首批子线程处理完成\n`)); 224 | isDone = true; 225 | }, 226 | }); 227 | } 228 | } 229 | }); 230 | Object.assign(STATS, stats); 231 | allFileListTodo = STATS.allFilePaths.slice(sendedToCpFileNum); 232 | 233 | let tip = `[${showCostTime(startTime)}] 目录预处理完成,发现目录总数:${color.yellow(STATS.totalDir)},文件总数:${color.yellowBright( 234 | STATS.totalFile, 235 | )}`; 236 | if (CONFIG.cpDuringStats && isDone) { 237 | tip += `。已处理了 ${color.yellow(STATS.totalFileHandler)} 个文件,其中复制了 ${color.magenta(STATS.totalFileNew)} 个文件`; 238 | } 239 | logInline(tip); 240 | 241 | const onProgress = (s: DfcStats) => { 242 | logProgress(true, s); 243 | if (cfg.onProgress) cfg.onProgress(STATS); 244 | }; 245 | const onEndCallback = (s: DfcStats) => { 246 | ['totalDirNew', 'totalFileNew', 'totalFileSize', 'totalFileNewSize'].forEach((key) => { 247 | if (s[key]) STATS[key] += s[key]; 248 | }); 249 | onEnd(); 250 | }; 251 | 252 | if (CONFIG.threads < 2 || STATS.totalFile < CONFIG.mutiThreadMinFiles) { 253 | logPrint(color.yellow('\n\n单线程执行')); 254 | fileCopy(STATS.allFilePaths, { 255 | onProgress, 256 | onEnd: onEndCallback, 257 | }); 258 | } else { 259 | mutiThreadCopy(allFileListTodo, { 260 | startTime, 261 | onStart: (threadNum) => { 262 | logPrint(color.cyan('\n\n开始多线程处理,线程数:'), color.green(threadNum)); 263 | }, 264 | onProgress, 265 | onEnd: onEndCallback, 266 | }); 267 | } 268 | } 269 | }); 270 | } 271 | 272 | export async function fastCopy(cfg: typeof CONFIG) { 273 | if (workerThreads.isMainThread) return startMain(cfg); 274 | log.red('只能以主线程模式启动'); 275 | return false; 276 | } 277 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | promises, 3 | existsSync, 4 | rmdirSync, 5 | statSync, 6 | readdirSync, 7 | createReadStream, 8 | createWriteStream, 9 | utimes, 10 | mkdirSync, 11 | utimesSync, 12 | type Stats, 13 | } from 'fs'; 14 | import { dirname, resolve } from 'path'; 15 | import * as readline from 'readline'; 16 | import { CONFIG } from './config'; 17 | import { color } from 'console-log-colors'; 18 | import type { DfcConfig, DfcStats, FsStatInfo } from './type'; 19 | 20 | /** 日志打印 */ 21 | export function logPrint(...args) { 22 | if (CONFIG.slient) return; 23 | console.log(...args); 24 | } 25 | 26 | /** 执行文件复制(获取到全部文件后) */ 27 | export async function fileCopy( 28 | filePathList: DfcStats['allFilePaths'], 29 | opts: { onProgress?: DfcConfig['onProgress']; onEnd?: DfcConfig['onEnd'] } = {}, 30 | ) { 31 | const stats: DfcStats = { 32 | totalFile: filePathList.length, 33 | totalFileSize: 0, 34 | totalFileHandler: 0, 35 | totalFileNew: 0, 36 | totalFileNewSize: 0, 37 | totalDirNew: 0, 38 | }; 39 | 40 | if (!filePathList) return stats; 41 | const progressTipNum = filePathList.length > 10000 ? 1000 : 100; 42 | const queueSize = 8; 43 | let cpFileQueue: Promise[] = []; 44 | 45 | for (const item of filePathList) { 46 | const { src: srcPath, dest: destPath, srcStat } = item; 47 | const check = await checkFile(srcPath, destPath, srcStat); 48 | 49 | stats.totalFileHandler++; 50 | 51 | if (stats.totalFileHandler > 1 && 0 === stats.totalFileHandler % progressTipNum) { 52 | if (opts.onProgress) opts.onProgress(stats); 53 | } 54 | 55 | if (check === 'dir') continue; 56 | stats.totalFileSize += srcStat.size; 57 | if (check === false) continue; 58 | 59 | try { 60 | // 创建目的文件的目录路径 61 | const destFileDir = dirname(destPath); 62 | if (!existsSync(destFileDir)) { 63 | cpDir(dirname(srcPath), destFileDir, srcStat); 64 | stats.totalDirNew++; 65 | } 66 | 67 | if (cpFileQueue.length >= queueSize) { 68 | await Promise.allSettled(cpFileQueue); 69 | cpFileQueue = []; 70 | } 71 | 72 | cpFileQueue.push(cpFile(srcPath, destPath, srcStat)); 73 | stats.totalFileNew++; 74 | stats.totalFileNewSize += srcStat.size; 75 | } catch (err) { 76 | console.log(`文件复制失败:\nsrc: ${srcPath}\ndest: ${destPath}\n`, err); 77 | } 78 | } 79 | 80 | await Promise.allSettled(cpFileQueue); 81 | 82 | if (opts.onEnd) opts.onEnd(stats); 83 | return stats; 84 | } 85 | 86 | export function formatTime(timeMs) { 87 | // return timeMs / 1000 + 's'; 88 | return new Date(new Date('1970-01-01T00:00:00').getTime() + timeMs).toTimeString().split(' ')[0]; 89 | } 90 | 91 | /** 显示从指定的时间到此刻花费的时间 */ 92 | export function showCostTime(startTime: number) { 93 | return color.cyan(formatTime(Date.now() - startTime)); 94 | } 95 | 96 | export function isExclude(srcFilePath: string, srcStat?: FsStatInfo) { 97 | for (const d of CONFIG.exclude) { 98 | if (d instanceof RegExp) { 99 | if (srcFilePath.match(d)) return true; 100 | } else { 101 | if (srcFilePath.includes(d)) return true; 102 | } 103 | } 104 | 105 | if (CONFIG.include.length > 0) { 106 | if (!srcStat) srcStat = toFSStatInfo(statSync(srcFilePath)); 107 | if (srcStat.isFile) { 108 | return !CONFIG.include.some((d) => (d instanceof RegExp ? srcFilePath.match(d) : srcFilePath.includes(d))); 109 | } 110 | } 111 | 112 | return false; 113 | } 114 | 115 | /** 116 | * 文件校验 117 | * @returns 118 | * 返回 null 表示文件或目录被忽略 119 | * 返回 false 表示文件或目录不执行处理 120 | */ 121 | export async function checkFile(srcFilePath: string, destFilePath: string, srcStat: FsStatInfo, config = CONFIG) { 122 | // console.debug('checkFile:', srcFilePath, destFilePath); 123 | if (isExclude(srcFilePath, srcStat)) return false; 124 | 125 | if (srcStat.isDirectory) return 'dir'; 126 | 127 | if (srcStat.mtime.getTime() < config.minDateTime) return false; 128 | 129 | // 相同大小的文件已存在 130 | if (config.skipSameFile) { 131 | if (existsSync(destFilePath) && statSync(destFilePath).size === srcStat.size) { 132 | if (CONFIG.deleteSrc) await promises.unlink(srcFilePath); 133 | return false; 134 | } 135 | } 136 | 137 | return srcStat; 138 | } 139 | 140 | /** 复制一个文件 */ 141 | export async function cpFile(srcPath, destPath, srcStat: FsStatInfo) { 142 | try { 143 | await promises.copyFile(srcPath, destPath); 144 | // writeFileSync(destPath, readFileSync(srcPath)); 145 | promises.utimes(destPath, srcStat.atime, srcStat.mtime); 146 | if (CONFIG.deleteSrc === true) await promises.unlink(srcPath); 147 | } catch (err) { 148 | console.log(`文件复制失败:\nsrc: ${srcPath}\ndest: ${destPath}\n`, err); 149 | } 150 | } 151 | 152 | /** 复制一个文件(stream 方式异步) */ 153 | export async function cpFilebyStream(srcPath, destPath, srcStat: FsStatInfo) { 154 | try { 155 | await new Promise((rs, reject) => { 156 | createReadStream(srcPath) 157 | .pipe(createWriteStream(destPath)) 158 | .on('close', () => { 159 | utimes(destPath, srcStat.atime, srcStat.mtime, (err) => { 160 | if (err) reject(err); 161 | else { 162 | if (CONFIG.deleteSrc === true) promises.unlink(srcPath); 163 | rs(true); 164 | } 165 | }); 166 | }); 167 | }); 168 | } catch (err) { 169 | console.log(`文件复制失败:\nsrc: ${srcPath}\ndest: ${destPath}\n`, err); 170 | } 171 | } 172 | 173 | /** 复制一个目录(不作任何检查以保证速度) */ 174 | export function cpDir(srcDir, destDir, srcStat?: FsStatInfo) { 175 | try { 176 | if (!srcStat) srcStat = toFSStatInfo(statSync(srcDir)); 177 | mkdirSync(destDir, { recursive: true }); 178 | utimesSync(destDir, srcStat.atime, srcStat.mtime); 179 | } catch (err) { 180 | console.log(`目录复制失败:\nsrc: ${srcDir}\ndest: ${destDir}\n`, err); 181 | } 182 | } 183 | 184 | export function toFSStatInfo(fstat: Stats) { 185 | const info: FsStatInfo = { 186 | isFile: fstat.isFile(), 187 | isDirectory: fstat.isDirectory(), 188 | nlink: fstat.nlink, 189 | atime: fstat.atime, 190 | mtime: fstat.mtime, 191 | size: fstat.size, 192 | }; 193 | return info; 194 | } 195 | 196 | /** 在当前行打印日志信息(主要用于显示进度信息) */ 197 | export function logInline(msg) { 198 | if (CONFIG.slient) return; 199 | // console.log(msg); 200 | readline.clearLine(process.stdout, 0); 201 | readline.cursorTo(process.stdout, 0); 202 | process.stdout.write(msg, 'utf-8'); 203 | } 204 | 205 | /** 获取所有需处理的文件列表(后续分割为多线程处理) */ 206 | export async function getAllFiles(_srcDir: string, _destDir = '', onProgress?: (typeof CONFIG)['onProgress']) { 207 | const stats: DfcStats = { 208 | totalFile: 0, 209 | totalDir: 0, 210 | allDirPaths: [], 211 | allFilePaths: [], 212 | }; 213 | let preProgressTime = Date.now(); 214 | 215 | const handler = async (srcDir: string, destDir = '') => { 216 | if (isExclude(srcDir)) return false; 217 | 218 | const filelist = await promises.readdir(srcDir, { encoding: 'utf8' }); 219 | const now = Date.now(); 220 | 221 | if (onProgress && now - preProgressTime > 500) { 222 | preProgressTime = now; 223 | onProgress(Object.assign({}, stats)); 224 | } 225 | 226 | const list = filelist.map(async (filename) => { 227 | if (!filename) return; 228 | 229 | const srcPath = resolve(srcDir, filename); 230 | const fstat = toFSStatInfo(statSync(srcPath)); 231 | if (isExclude(srcPath, fstat) || !existsSync(srcPath)) return; 232 | 233 | const destPath = destDir ? resolve(destDir, filename) : ''; 234 | const info: DfcStats['allDirPaths'][0] = { 235 | src: srcPath, 236 | dest: destPath, 237 | srcStat: fstat, 238 | }; 239 | 240 | if (fstat.isDirectory) { 241 | stats.totalDir++; 242 | stats.allDirPaths.push(info); 243 | return handler(srcPath, destPath); 244 | } else { 245 | stats.totalFile++; 246 | stats.allFilePaths.push(info); 247 | } 248 | }); 249 | 250 | return Promise.all(list); 251 | return true; 252 | }; 253 | 254 | await handler(_srcDir, _destDir); 255 | return stats; 256 | } 257 | 258 | /** 单线程模式,执行目录复制(递归) */ 259 | export function dirCopyRecursive(src: string, dest: string, onProgress?: (stats) => void) { 260 | const stats: DfcStats = { 261 | totalFile: 0, // 文件总数 262 | totalFileSize: 0, 263 | totalFileHandler: 0, // 已处理的文件数 264 | totalFileNew: 0, // 复制了多少个文件 265 | totalFileNewSize: 0, 266 | totalDirNew: 0, // 创建了多少个目录 267 | totalDir: 0, 268 | }; 269 | 270 | const handler = async (srcDir: string, destDir: string, srcDirStat?: FsStatInfo) => { 271 | if (!existsSync(destDir)) { 272 | cpDir(srcDir, destDir, srcDirStat); 273 | stats.totalDirNew++; 274 | } 275 | 276 | const filelist = readdirSync(srcDir, { encoding: 'utf8' }); 277 | let srcPath = ''; 278 | let destPath = ''; 279 | 280 | for (const filename of filelist) { 281 | if (!filename || filename === '..') continue; 282 | 283 | onProgress && onProgress(stats); 284 | srcPath = resolve(srcDir, filename); 285 | destPath = resolve(destDir, filename); 286 | 287 | const srcStat = statSync(srcPath); 288 | const statInfo = toFSStatInfo(srcStat); 289 | const check = await checkFile(srcPath, destPath, statInfo); 290 | 291 | if (srcStat.isFile()) { 292 | stats.totalFileHandler++; 293 | stats.totalFile = stats.totalFileHandler; 294 | stats.totalFileSize += srcStat.size; 295 | } else { 296 | stats.totalDir++; 297 | } 298 | 299 | if (!check) continue; 300 | 301 | if (check === 'dir') { 302 | await handler(srcPath, destPath, statInfo); 303 | // 移除空的文件夹 304 | if (!readdirSync(destPath).length) { 305 | rmdirSync(destPath); 306 | stats.totalDirNew--; 307 | } 308 | continue; 309 | } 310 | 311 | await cpFile(srcPath, destPath, statInfo); 312 | stats.totalFileNew++; 313 | stats.totalFileNewSize += srcStat.size; 314 | } 315 | 316 | // 移除空目录 317 | if (!readdirSync(srcDir).length) { 318 | await promises.rm(srcDir, { recursive: true, force: true }); 319 | } 320 | }; 321 | 322 | handler(src, dest); 323 | stats.totalFile = stats.totalFileHandler; 324 | return stats; 325 | } 326 | 327 | /** 等待并获取用户输入内容 */ 328 | export function readSyncByRl(tips: string) { 329 | tips = tips || '> '; 330 | 331 | return new Promise((resolve) => { 332 | const rl = readline.createInterface({ 333 | input: process.stdin, 334 | output: process.stdout, 335 | }); 336 | 337 | rl.question(tips, (answer) => { 338 | resolve(answer.trim()); 339 | rl.close(); 340 | }); 341 | }); 342 | } 343 | 344 | export function formatFileSize(size: number) { 345 | if (size > 1 << 30) return (size / (1 << 30)).toFixed(2) + 'G'; 346 | if (size > 1 << 20) return (size / (1 << 20)).toFixed(2) + 'M'; 347 | if (size > 1 << 10) return (size / (1 << 10)).toFixed(2) + 'KB'; 348 | return size + 'B'; 349 | } 350 | --------------------------------------------------------------------------------