├── .editorconfig ├── .gitattributes ├── .github ├── funding.yml ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── copy-file-error.js ├── fs.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test ├── async.js ├── helpers ├── _assert.js └── _fs-errors.js ├── progress.js └── sync.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: sindresorhus 2 | open_collective: sindresorhus 3 | tidelift: npm/cp-file 4 | custom: https://sindresorhus.com/donate 5 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | .nyc_output 4 | coverage 5 | temp 6 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /copy-file-error.js: -------------------------------------------------------------------------------- 1 | export default class CopyFileError extends Error { 2 | constructor(message, {cause} = {}) { 3 | super(message, {cause}); 4 | Object.assign(this, cause); 5 | this.name = 'CopyFileError'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /fs.js: -------------------------------------------------------------------------------- 1 | import {promisify} from 'node:util'; 2 | import fs from 'graceful-fs'; 3 | import {pEvent} from 'p-event'; 4 | import CopyFileError from './copy-file-error.js'; 5 | 6 | const statP = promisify(fs.stat); 7 | const lstatP = promisify(fs.lstat); 8 | const utimesP = promisify(fs.utimes); 9 | const chmodP = promisify(fs.chmod); 10 | const makeDirectoryP = promisify(fs.mkdir); 11 | 12 | export const closeSync = fs.closeSync.bind(fs); 13 | export const createWriteStream = fs.createWriteStream.bind(fs); 14 | 15 | export async function createReadStream(path, options) { 16 | const read = fs.createReadStream(path, options); 17 | 18 | try { 19 | await pEvent(read, ['readable', 'end']); 20 | } catch (error) { 21 | throw new CopyFileError(`Cannot read from \`${path}\`: ${error.message}`, {cause: error}); 22 | } 23 | 24 | return read; 25 | } 26 | 27 | export const stat = path => statP(path).catch(error => { 28 | throw new CopyFileError(`Cannot stat path \`${path}\`: ${error.message}`, {cause: error}); 29 | }); 30 | 31 | export const lstat = path => lstatP(path).catch(error => { 32 | throw new CopyFileError(`lstat \`${path}\` failed: ${error.message}`, {cause: error}); 33 | }); 34 | 35 | export const utimes = (path, atime, mtime) => utimesP(path, atime, mtime).catch(error => { 36 | throw new CopyFileError(`utimes \`${path}\` failed: ${error.message}`, {cause: error}); 37 | }); 38 | 39 | export const chmod = (path, mode) => chmodP(path, mode).catch(error => { 40 | throw new CopyFileError(`chmod \`${path}\` failed: ${error.message}`, {cause: error}); 41 | }); 42 | 43 | export const statSync = path => { 44 | try { 45 | return fs.statSync(path); 46 | } catch (error) { 47 | throw new CopyFileError(`stat \`${path}\` failed: ${error.message}`, {cause: error}); 48 | } 49 | }; 50 | 51 | export const lstatSync = path => { 52 | try { 53 | return fs.statSync(path); 54 | } catch (error) { 55 | throw new CopyFileError(`stat \`${path}\` failed: ${error.message}`, {cause: error}); 56 | } 57 | }; 58 | 59 | export const utimesSync = (path, atime, mtime) => { 60 | try { 61 | return fs.utimesSync(path, atime, mtime); 62 | } catch (error) { 63 | throw new CopyFileError(`utimes \`${path}\` failed: ${error.message}`, {cause: error}); 64 | } 65 | }; 66 | 67 | export const makeDirectory = (path, options) => makeDirectoryP(path, {...options, recursive: true}).catch(error => { 68 | throw new CopyFileError(`Cannot create directory \`${path}\`: ${error.message}`, {cause: error}); 69 | }); 70 | 71 | export const makeDirectorySync = (path, options) => { 72 | try { 73 | fs.mkdirSync(path, {...options, recursive: true}); 74 | } catch (error) { 75 | throw new CopyFileError(`Cannot create directory \`${path}\`: ${error.message}`, {cause: error}); 76 | } 77 | }; 78 | 79 | export const copyFileSync = (source, destination, flags) => { 80 | try { 81 | fs.copyFileSync(source, destination, flags); 82 | } catch (error) { 83 | throw new CopyFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, {cause: error}); 84 | } 85 | }; 86 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type Options = { 2 | /** 3 | Overwrite existing destination file. 4 | 5 | @default true 6 | */ 7 | readonly overwrite?: boolean; 8 | 9 | /** 10 | [Permissions](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) for created directories. 11 | 12 | It has no effect on Windows. 13 | 14 | @default 0o777 15 | */ 16 | readonly directoryMode?: number; 17 | 18 | /** 19 | The working directory to find source files. 20 | 21 | The source and destination path are relative to this. 22 | 23 | @default process.cwd() 24 | */ 25 | readonly cwd?: string; 26 | }; 27 | 28 | export type AsyncOptions = { 29 | /** 30 | The given function is called whenever there is measurable progress. 31 | 32 | Note: For empty files, the `onProgress` event is emitted only once. 33 | 34 | @example 35 | ``` 36 | import {copyFile} from 'copy-file'; 37 | 38 | await copyFile('source/unicorn.png', 'destination/unicorn.png', { 39 | onProgress: progress => { 40 | // … 41 | } 42 | }); 43 | ``` 44 | */ 45 | readonly onProgress?: (progress: ProgressData) => void; 46 | }; 47 | 48 | export type ProgressData = { 49 | /** 50 | Absolute path to source. 51 | */ 52 | sourcePath: string; 53 | 54 | /** 55 | Absolute path to destination. 56 | */ 57 | destinationPath: string; 58 | 59 | /** 60 | File size in bytes. 61 | */ 62 | size: number; 63 | 64 | /** 65 | Copied size in bytes. 66 | */ 67 | writtenBytes: number; 68 | 69 | /** 70 | Copied percentage, a value between `0` and `1`. 71 | */ 72 | percent: number; 73 | }; 74 | 75 | /** 76 | Copy a file. 77 | 78 | @param source - The file you want to copy. 79 | @param destination - Where you want the file copied. 80 | @returns A `Promise` that resolves when the file is copied. 81 | 82 | The file is cloned if the `onProgress` option is not passed and the [file system supports it](https://stackoverflow.com/a/76496347/64949). 83 | 84 | @example 85 | ``` 86 | import {copyFile} from 'copy-file'; 87 | 88 | await copyFile('source/unicorn.png', 'destination/unicorn.png'); 89 | console.log('File copied'); 90 | ``` 91 | */ 92 | export function copyFile(source: string, destination: string, options?: Options & AsyncOptions): Promise; 93 | 94 | /** 95 | Copy a file synchronously. 96 | 97 | @param source - The file you want to copy. 98 | @param destination - Where you want the file copied. 99 | 100 | The file is cloned if the [file system supports it](https://stackoverflow.com/a/76496347/64949). 101 | 102 | @example 103 | ``` 104 | import {copyFileSync} from 'copy-file'; 105 | 106 | copyFileSync('source/unicorn.png', 'destination/unicorn.png'); 107 | ``` 108 | */ 109 | export function copyFileSync(source: string, destination: string, options?: Options): void; 110 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import realFS, {constants as fsConstants} from 'node:fs'; 3 | import realFSPromises from 'node:fs/promises'; 4 | import {pEvent} from 'p-event'; 5 | import CopyFileError from './copy-file-error.js'; 6 | import * as fs from './fs.js'; 7 | 8 | const resolvePath = (cwd, sourcePath, destinationPath) => ({ 9 | sourcePath: path.resolve(cwd, sourcePath), 10 | destinationPath: path.resolve(cwd, destinationPath), 11 | }); 12 | 13 | const checkSourceIsFile = (stat, source) => { 14 | if (!stat.isFile()) { 15 | throw Object.assign(new CopyFileError(`EISDIR: illegal operation on a directory '${source}'`), { 16 | errno: -21, 17 | code: 'EISDIR', 18 | source, 19 | }); 20 | } 21 | }; 22 | 23 | export async function copyFile(sourcePath, destinationPath, options = {}) { 24 | if (!sourcePath || !destinationPath) { 25 | throw new CopyFileError('`source` and `destination` required'); 26 | } 27 | 28 | if (options.cwd) { 29 | ({sourcePath, destinationPath} = resolvePath(options.cwd, sourcePath, destinationPath)); 30 | } 31 | 32 | options = { 33 | overwrite: true, 34 | ...options, 35 | }; 36 | 37 | const stats = await fs.lstat(sourcePath); 38 | const {size} = stats; 39 | checkSourceIsFile(stats, sourcePath); 40 | await fs.makeDirectory(path.dirname(destinationPath), {mode: options.directoryMode}); 41 | 42 | if (typeof options.onProgress === 'function') { 43 | const readStream = await fs.createReadStream(sourcePath); 44 | const writeStream = fs.createWriteStream(destinationPath, {flags: options.overwrite ? 'w' : 'wx'}); 45 | 46 | const emitProgress = writtenBytes => { 47 | options.onProgress({ 48 | sourcePath: path.resolve(sourcePath), 49 | destinationPath: path.resolve(destinationPath), 50 | size, 51 | writtenBytes, 52 | percent: writtenBytes === size ? 1 : writtenBytes / size, 53 | }); 54 | }; 55 | 56 | readStream.on('data', () => { 57 | emitProgress(writeStream.bytesWritten); 58 | }); 59 | 60 | let readError; 61 | 62 | readStream.once('error', error => { 63 | readError = new CopyFileError(`Cannot read from \`${sourcePath}\`: ${error.message}`, {cause: error}); 64 | }); 65 | 66 | let shouldUpdateStats = false; 67 | try { 68 | const writePromise = pEvent(writeStream, 'close'); 69 | readStream.pipe(writeStream); 70 | await writePromise; 71 | emitProgress(size); 72 | shouldUpdateStats = true; 73 | } catch (error) { 74 | throw new CopyFileError(`Cannot write to \`${destinationPath}\`: ${error.message}`, {cause: error}); 75 | } 76 | 77 | if (readError) { 78 | throw readError; 79 | } 80 | 81 | if (shouldUpdateStats) { 82 | const stats = await fs.lstat(sourcePath); 83 | 84 | return Promise.all([ 85 | fs.utimes(destinationPath, stats.atime, stats.mtime), 86 | fs.chmod(destinationPath, stats.mode), 87 | ]); 88 | } 89 | } else { 90 | // eslint-disable-next-line no-bitwise 91 | const flags = options.overwrite ? fsConstants.COPYFILE_FICLONE : (fsConstants.COPYFILE_FICLONE | fsConstants.COPYFILE_EXCL); 92 | 93 | try { 94 | await realFSPromises.copyFile(sourcePath, destinationPath, flags); 95 | 96 | await Promise.all([ 97 | realFSPromises.utimes(destinationPath, stats.atime, stats.mtime), 98 | realFSPromises.chmod(destinationPath, stats.mode), 99 | ]); 100 | } catch (error) { 101 | throw new CopyFileError(error.message, {cause: error}); 102 | } 103 | } 104 | } 105 | 106 | export function copyFileSync(sourcePath, destinationPath, options = {}) { 107 | if (!sourcePath || !destinationPath) { 108 | throw new CopyFileError('`source` and `destination` required'); 109 | } 110 | 111 | if (options.cwd) { 112 | ({sourcePath, destinationPath} = resolvePath(options.cwd, sourcePath, destinationPath)); 113 | } 114 | 115 | options = { 116 | overwrite: true, 117 | ...options, 118 | }; 119 | 120 | const stats = fs.lstatSync(sourcePath); 121 | checkSourceIsFile(stats, sourcePath); 122 | fs.makeDirectorySync(path.dirname(destinationPath), {mode: options.directoryMode}); 123 | 124 | // eslint-disable-next-line no-bitwise 125 | const flags = options.overwrite ? fsConstants.COPYFILE_FICLONE : (fsConstants.COPYFILE_FICLONE | fsConstants.COPYFILE_EXCL); 126 | try { 127 | realFS.copyFileSync(sourcePath, destinationPath, flags); 128 | } catch (error) { 129 | throw new CopyFileError(error.message, {cause: error}); 130 | } 131 | 132 | fs.utimesSync(destinationPath, stats.atime, stats.mtime); 133 | fs.chmod(destinationPath, stats.mode); 134 | } 135 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectError, expectType} from 'tsd'; 2 | import {copyFile, copyFileSync, type ProgressData} from './index.js'; 3 | 4 | expectType >( 5 | copyFile('source/unicorn.png', 'destination/unicorn.png'), 6 | ); 7 | expectType>( 8 | copyFile('source/unicorn.png', 'destination/unicorn.png', {overwrite: false}), 9 | ); 10 | expectType>( 11 | copyFile('source/unicorn.png', 'destination/unicorn.png', { 12 | directoryMode: 0o700, 13 | }), 14 | ); 15 | expectError( 16 | await copyFile('source/unicorn.png', 'destination/unicorn.png', { 17 | directoryMode: '700', 18 | }), 19 | ); 20 | expectType>( 21 | copyFile('source/unicorn.png', 'destination/unicorn.png', { 22 | onProgress(progress) { 23 | expectType(progress); 24 | expectType(progress.sourcePath); 25 | expectType(progress.destinationPath); 26 | expectType(progress.size); 27 | expectType(progress.writtenBytes); 28 | expectType(progress.percent); 29 | }, 30 | }), 31 | ); 32 | 33 | expectType(copyFileSync('source/unicorn.png', 'destination/unicorn.png')); 34 | expectType( 35 | copyFileSync('source/unicorn.png', 'destination/unicorn.png', { 36 | overwrite: false, 37 | }), 38 | ); 39 | expectType( 40 | copyFileSync('source/unicorn.png', 'destination/unicorn.png', { 41 | directoryMode: 0o700, 42 | }), 43 | ); 44 | expectError( 45 | copyFileSync('source/unicorn.png', 'destination/unicorn.png', { 46 | directoryMode: '700', 47 | }), 48 | ); 49 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "copy-file", 3 | "version": "11.0.0", 4 | "description": "Copy a file", 5 | "license": "MIT", 6 | "repository": "sindresorhus/copy-file", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "engines": { 19 | "node": ">=18" 20 | }, 21 | "scripts": { 22 | "test": "xo && nyc ava && tsd" 23 | }, 24 | "files": [ 25 | "index.js", 26 | "index.d.ts", 27 | "copy-file-error.js", 28 | "fs.js" 29 | ], 30 | "keywords": [ 31 | "copy", 32 | "copying", 33 | "cp", 34 | "file", 35 | "clone", 36 | "fs", 37 | "stream", 38 | "file-system", 39 | "filesystem", 40 | "ncp", 41 | "fast", 42 | "quick", 43 | "data", 44 | "content", 45 | "contents", 46 | "read", 47 | "write", 48 | "io" 49 | ], 50 | "dependencies": { 51 | "graceful-fs": "^4.2.11", 52 | "p-event": "^6.0.0" 53 | }, 54 | "devDependencies": { 55 | "ava": "^5.3.1", 56 | "clear-module": "^4.1.2", 57 | "coveralls": "^3.1.1", 58 | "del": "^7.1.0", 59 | "import-fresh": "^3.3.0", 60 | "nyc": "^15.1.0", 61 | "sinon": "^17.0.1", 62 | "tsd": "^0.29.0", 63 | "xo": "^0.56.0" 64 | }, 65 | "xo": { 66 | "rules": { 67 | "ava/assertion-arguments": "off" 68 | } 69 | }, 70 | "ava": { 71 | "workerThreads": false, 72 | "serial": true 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # copy-file 2 | 3 | > Copy a file 4 | 5 | ## Highlights 6 | 7 | - It's super fast by [cloning](https://stackoverflow.com/questions/71629903/node-js-why-we-should-use-copyfile-ficlone-and-copyfile-ficlone-force-what-is) the file whenever possible. 8 | - Resilient by using [graceful-fs](https://github.com/isaacs/node-graceful-fs). 9 | - User-friendly by creating non-existent destination directories for you. 10 | - Can be safe by turning off [overwriting](#overwrite). 11 | - Preserves file mode [but not ownership](https://github.com/sindresorhus/copy-file/issues/22#issuecomment-502079547). 12 | - User-friendly errors. 13 | 14 | ## Install 15 | 16 | ```sh 17 | npm install copy-file 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```js 23 | import {copyFile} from 'copy-file'; 24 | 25 | await copyFile('source/unicorn.png', 'destination/unicorn.png'); 26 | console.log('File copied'); 27 | ``` 28 | 29 | ## API 30 | 31 | ### copyFile(source, destination, options?) 32 | 33 | Returns a `Promise` that resolves when the file is copied. 34 | 35 | The file is cloned if the `onProgress` option is not passed and the [file system supports it](https://stackoverflow.com/a/76496347/64949). 36 | 37 | ### copyFileSync(source, destination, options?) 38 | 39 | #### source 40 | 41 | Type: `string` 42 | 43 | The file you want to copy. 44 | 45 | The file is cloned if the [file system supports it](https://stackoverflow.com/a/76496347/64949). 46 | 47 | #### destination 48 | 49 | Type: `string` 50 | 51 | Where you want the file copied. 52 | 53 | #### options 54 | 55 | Type: `object` 56 | 57 | ##### overwrite 58 | 59 | Type: `boolean`\ 60 | Default: `true` 61 | 62 | Overwrite existing destination file. 63 | 64 | ##### cwd 65 | 66 | Type: `string`\ 67 | Default: `process.cwd()` 68 | 69 | The working directory to find source files. 70 | 71 | The source and destination path are relative to this. 72 | 73 | ##### directoryMode 74 | 75 | Type: `number`\ 76 | Default: `0o777` 77 | 78 | [Permissions](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) for created directories. 79 | 80 | It has no effect on Windows. 81 | 82 | ##### onProgress 83 | 84 | Type: `(progress: ProgressData) => void` 85 | 86 | The given function is called whenever there is measurable progress. 87 | 88 | Only available when using the async method. 89 | 90 | ###### `ProgressData` 91 | 92 | ```js 93 | { 94 | sourcePath: string, 95 | destinationPath: string, 96 | size: number, 97 | writtenBytes: number, 98 | percent: number 99 | } 100 | ``` 101 | 102 | - `sourcePath` and `destinationPath` are absolute paths. 103 | - `size` and `writtenBytes` are in bytes. 104 | - `percent` is a value between `0` and `1`. 105 | 106 | ###### Notes 107 | 108 | - For empty files, the `onProgress` callback function is emitted only once. 109 | 110 | ```js 111 | import {copyFile} from 'copy-file'; 112 | 113 | await copyFile(source, destination, { 114 | onProgress: progress => { 115 | // … 116 | } 117 | }); 118 | ``` 119 | 120 | ## Related 121 | 122 | - [cpy](https://github.com/sindresorhus/cpy) - Copy files 123 | - [cpy-cli](https://github.com/sindresorhus/cpy-cli) - Copy files on the command-line 124 | - [move-file](https://github.com/sindresorhus/move-file) - Move a file 125 | - [make-dir](https://github.com/sindresorhus/make-dir) - Make a directory and its parents if needed 126 | -------------------------------------------------------------------------------- /test/async.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import crypto from 'node:crypto'; 3 | import path from 'node:path'; 4 | import fs from 'node:fs'; 5 | import {fileURLToPath} from 'node:url'; 6 | import importFresh from 'import-fresh'; 7 | import clearModule from 'clear-module'; 8 | import {deleteSync} from 'del'; 9 | import test from 'ava'; 10 | import sinon from 'sinon'; 11 | import {copyFile} from '../index.js'; 12 | import assertDateEqual from './helpers/_assert.js'; 13 | import {buildEACCES, buildENOSPC, buildENOENT, buildEPERM, buildERRSTREAMWRITEAFTEREND} from './helpers/_fs-errors.js'; 14 | 15 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 16 | 17 | const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; 18 | 19 | test.before(() => { 20 | process.chdir(path.dirname(__dirname)); 21 | deleteSync('temp'); // In case last test run failed. 22 | fs.mkdirSync('temp'); 23 | }); 24 | 25 | test.after(() => { 26 | deleteSync('temp'); 27 | }); 28 | 29 | test.beforeEach(t => { 30 | t.context.source = path.join('temp', crypto.randomUUID()); 31 | t.context.destination = path.join('temp', crypto.randomUUID()); 32 | }); 33 | 34 | test('reject an Error on missing `source`', async t => { 35 | await t.throwsAsync(copyFile(), { 36 | message: /`source`/, 37 | }); 38 | }); 39 | 40 | test('reject an Error on missing `destination`', async t => { 41 | await t.throwsAsync(copyFile('TARGET'), { 42 | message: /`destination`/, 43 | }); 44 | }); 45 | 46 | test('copy a file', async t => { 47 | await copyFile('license', t.context.destination); 48 | t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); 49 | }); 50 | 51 | test('copy an empty file', async t => { 52 | fs.writeFileSync(t.context.source, ''); 53 | await copyFile(t.context.source, t.context.destination); 54 | t.is(fs.readFileSync(t.context.destination, 'utf8'), ''); 55 | }); 56 | 57 | test('copy big files', async t => { 58 | const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); 59 | fs.writeFileSync(t.context.source, buffer); 60 | await copyFile(t.context.source, t.context.destination); 61 | t.true(buffer.equals(fs.readFileSync(t.context.destination))); 62 | }); 63 | 64 | test('do not alter overwrite option', async t => { 65 | const options = {}; 66 | await copyFile('license', t.context.destination, options); 67 | t.false('overwrite' in options); 68 | }); 69 | 70 | test('overwrite when enabled', async t => { 71 | fs.writeFileSync(t.context.destination, ''); 72 | await copyFile('license', t.context.destination, {overwrite: true}); 73 | t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); 74 | }); 75 | 76 | test('overwrite when options are undefined', async t => { 77 | fs.writeFileSync(t.context.destination, ''); 78 | await copyFile('license', t.context.destination); 79 | t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); 80 | }); 81 | 82 | test('do not overwrite when disabled', async t => { 83 | fs.writeFileSync(t.context.destination, ''); 84 | const error = await t.throwsAsync(copyFile('license', t.context.destination, {overwrite: false})); 85 | t.is(error.name, 'CopyFileError', error.message); 86 | t.is(error.code, 'EEXIST', error.message); 87 | }); 88 | 89 | if (process.platform !== 'win32') { 90 | test('create directories with specified mode', async t => { 91 | const directory = t.context.destination; 92 | const destination = `${directory}/${crypto.randomUUID()}`; 93 | const directoryMode = 0o700; 94 | await copyFile('license', destination, {directoryMode}); 95 | const stat = fs.statSync(directory); 96 | t.is(stat.mode & directoryMode, directoryMode); // eslint-disable-line no-bitwise 97 | }); 98 | } 99 | 100 | test('do not create `destination` on unreadable `source`', async t => { 101 | const error = await t.throwsAsync(copyFile('node_modules', t.context.destination)); 102 | 103 | t.is(error.name, 'CopyFileError', error.message); 104 | t.is(error.code, 'EISDIR', error.message); 105 | 106 | t.throws(() => { 107 | fs.statSync(t.context.destination); 108 | }, { 109 | message: /ENOENT/, 110 | }); 111 | }); 112 | 113 | test('do not create `destination` directory on unreadable `source`', async t => { 114 | const error = await t.throwsAsync(copyFile('node_modules', path.join('temp/subdir', crypto.randomUUID()))); 115 | 116 | t.is(error.name, 'CopyFileError', error.message); 117 | t.is(error.code, 'EISDIR', error.message); 118 | t.false(fs.existsSync('subdir')); 119 | }); 120 | 121 | test('preserve timestamps', async t => { 122 | await copyFile('license', t.context.destination); 123 | const licenseStats = fs.lstatSync('license'); 124 | const temporaryStats = fs.lstatSync(t.context.destination); 125 | assertDateEqual(t, licenseStats.atime, temporaryStats.atime); 126 | assertDateEqual(t, licenseStats.mtime, temporaryStats.mtime); 127 | }); 128 | 129 | test('preserve mode', async t => { 130 | await copyFile('license', t.context.destination); 131 | const licenseStats = fs.lstatSync('license'); 132 | const temporaryStats = fs.lstatSync(t.context.destination); 133 | t.is(licenseStats.mode, temporaryStats.mode); 134 | }); 135 | 136 | test('throw an Error if `source` does not exists', async t => { 137 | const error = await t.throwsAsync(copyFile('NO_ENTRY', t.context.destination)); 138 | t.is(error.name, 'CopyFileError', error.message); 139 | t.is(error.code, 'ENOENT', error.message); 140 | t.regex(error.message, /`NO_ENTRY`/, error.message); 141 | t.regex(error.stack, /`NO_ENTRY`/, error.message); 142 | }); 143 | 144 | test.serial.failing('rethrow mkdir EACCES errors', async t => { 145 | const directoryPath = `/root/NO_ACCESS_${crypto.randomUUID()}`; 146 | const destination = path.join(directoryPath, crypto.randomUUID()); 147 | const mkdirError = buildEACCES(directoryPath); 148 | 149 | fs.stat = sinon.stub(fs, 'stat').throws(mkdirError); 150 | fs.mkdir = sinon.stub(fs, 'mkdir').throws(mkdirError); 151 | 152 | const error = await t.throwsAsync(copyFile('license', destination)); 153 | t.is(error.name, 'CopyFileError', error.message); 154 | t.is(error.errno, mkdirError.errno, error.message); 155 | t.is(error.code, mkdirError.code, error.message); 156 | t.is(error.path, mkdirError.path, error.message); 157 | t.true(fs.mkdir.called || fs.stat.called); 158 | 159 | fs.mkdir.restore(); 160 | fs.stat.restore(); 161 | }); 162 | 163 | test.serial.failing('rethrow ENOSPC errors', async t => { 164 | const {createWriteStream} = fs; 165 | const noSpaceError = buildENOSPC(); 166 | let isCalled = false; 167 | 168 | fs.createWriteStream = (path, options) => { 169 | const stream = createWriteStream(path, options); 170 | if (path === t.context.destination) { 171 | stream.on('pipe', () => { 172 | if (!isCalled) { 173 | isCalled = true; 174 | stream.emit('error', noSpaceError); 175 | } 176 | }); 177 | } 178 | 179 | return stream; 180 | }; 181 | 182 | clearModule('../fs.js'); 183 | const uncached = importFresh('../index.js'); 184 | const error = await t.throwsAsync(uncached('license', t.context.destination)); 185 | t.is(error.name, 'CopyFileError', error.message); 186 | t.is(error.errno, noSpaceError.errno, error.message); 187 | t.is(error.code, noSpaceError.code, error.message); 188 | t.true(isCalled); 189 | 190 | fs.createWriteStream = createWriteStream; 191 | }); 192 | 193 | test.serial.failing('rethrow stat errors', async t => { 194 | const fstatError = buildENOENT(); 195 | 196 | fs.writeFileSync(t.context.source, ''); 197 | fs.lstat = sinon.stub(fs, 'lstat').throws(fstatError); 198 | 199 | clearModule('../fs.js'); 200 | const uncached = importFresh('../index.js'); 201 | const error = await t.throwsAsync(uncached(t.context.source, t.context.destination)); 202 | t.is(error.name, 'CopyFileError', error.message); 203 | t.is(error.errno, fstatError.errno, error.message); 204 | t.is(error.code, fstatError.code, error.message); 205 | t.true(fs.lstat.called); 206 | 207 | fs.lstat.restore(); 208 | }); 209 | 210 | test.serial.failing('rethrow utimes errors', async t => { 211 | const utimesError = buildENOENT(); 212 | 213 | fs.utimes = sinon.stub(fs, 'utimes').throws(utimesError); 214 | 215 | clearModule('../fs.js'); 216 | const uncached = importFresh('../index.js'); 217 | const error = await t.throwsAsync(uncached('license', t.context.destination)); 218 | t.is(error.name, 'CopyFileError', error.message); 219 | t.is(error.code, 'ENOENT', error.message); 220 | t.true(fs.utimes.called); 221 | 222 | fs.utimes.restore(); 223 | }); 224 | 225 | test.serial.failing('rethrow chmod errors', async t => { 226 | const chmodError = buildEPERM(t.context.destination, 'chmod'); 227 | 228 | fs.chmod = sinon.stub(fs, 'chmod').throws(chmodError); 229 | 230 | clearModule('../fs.js'); 231 | const uncached = importFresh('../index.js'); 232 | const error = await t.throwsAsync(uncached('license', t.context.destination)); 233 | t.is(error.name, 'CopyFileError', error.message); 234 | t.is(error.code, chmodError.code, error.message); 235 | t.is(error.path, chmodError.path, error.message); 236 | t.true(fs.chmod.called); 237 | 238 | fs.chmod.restore(); 239 | }); 240 | 241 | test.serial.failing('rethrow read after open errors', async t => { 242 | const {createWriteStream, createReadStream} = fs; 243 | let calledWriteEnd = 0; 244 | let readStream; 245 | const readError = buildERRSTREAMWRITEAFTEREND(); 246 | 247 | fs.createWriteStream = (...arguments_) => { 248 | const stream = createWriteStream(...arguments_); 249 | const {end} = stream; 250 | 251 | stream.on('pipe', () => { 252 | readStream.emit('error', readError); 253 | }); 254 | 255 | stream.end = (...endArgs) => { 256 | calledWriteEnd++; 257 | return end.apply(stream, endArgs); 258 | }; 259 | 260 | return stream; 261 | }; 262 | 263 | fs.createReadStream = (...arguments_) => { 264 | /* Fake stream */ 265 | readStream = createReadStream(...arguments_); 266 | readStream.pause(); 267 | 268 | return readStream; 269 | }; 270 | 271 | clearModule('../fs.js'); 272 | const uncached = importFresh('../index.js'); 273 | const error = await t.throwsAsync(uncached('license', t.context.destination)); 274 | t.is(error.name, 'CopyFileError', error.message); 275 | t.is(error.code, readError.code, error.message); 276 | t.is(error.errno, readError.errno, error.message); 277 | t.is(calledWriteEnd, 1); 278 | 279 | Object.assign(fs, {createWriteStream, createReadStream}); 280 | }); 281 | 282 | test('cwd option', async t => { 283 | const error = await t.throwsAsync(copyFile('sync.js', t.context.destination)); 284 | 285 | t.is(error.name, 'CopyFileError'); 286 | t.is(error.code, 'ENOENT'); 287 | 288 | await t.notThrowsAsync(copyFile('sync.js', t.context.destination, {cwd: 'test'})); 289 | }); 290 | -------------------------------------------------------------------------------- /test/helpers/_assert.js: -------------------------------------------------------------------------------- 1 | /** 2 | Tests equality of Date objects, w/o considering milliseconds. 3 | 4 | @see {@link https://github.com/joyent/node/issues/7000|File timestamp resolution is inconsistent with fs.stat / fs.utimes} 5 | 6 | @param {Object} t - AVA's t 7 | @param {*} actual - the actual value 8 | @param {*} expected - the expected value 9 | @param {*} message - error message 10 | */ 11 | export default function assertDateEqual(t, actual, expected, message) { 12 | actual = new Date(actual); 13 | expected = new Date(expected); 14 | 15 | actual.setMilliseconds(0); 16 | expected.setMilliseconds(0); 17 | 18 | t.is(actual.getTime(), expected.getTime(), message); 19 | } 20 | -------------------------------------------------------------------------------- /test/helpers/_fs-errors.js: -------------------------------------------------------------------------------- 1 | export const buildEACCES = path => Object.assign(new Error(`EACCES: permission denied '${path}'`), { 2 | errno: -13, 3 | code: 'EACCES', 4 | path, 5 | }); 6 | 7 | export const buildENOSPC = () => Object.assign(new Error('ENOSPC, write'), { 8 | errno: -28, 9 | code: 'ENOSPC', 10 | }); 11 | 12 | export const buildENOENT = path => Object.assign(new Error(`ENOENT: no such file or directory '${path}'`), { 13 | errno: -2, 14 | code: 'ENOENT', 15 | path, 16 | }); 17 | 18 | export const buildERRSTREAMWRITEAFTEREND = () => Object.assign(new Error('ERR_STREAM_WRITE_AFTER_END'), { 19 | code: 'ERR_STREAM_WRITE_AFTER_END', 20 | }); 21 | 22 | export const buildEBADF = () => Object.assign(new Error('EBADF: bad file descriptor'), { 23 | errno: -9, 24 | code: 'EBADF', 25 | }); 26 | 27 | export const buildEPERM = (path, method) => Object.assign(new Error(`EPERM: ${method} '${path}''`), { 28 | errno: 50, 29 | code: 'EPERM', 30 | }); 31 | -------------------------------------------------------------------------------- /test/progress.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import crypto from 'node:crypto'; 3 | import path from 'node:path'; 4 | import {fileURLToPath} from 'node:url'; 5 | import fs from 'node:fs'; 6 | import {deleteSync} from 'del'; 7 | import test from 'ava'; 8 | import {copyFile} from '../index.js'; 9 | 10 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 11 | 12 | const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; 13 | 14 | test.before(() => { 15 | process.chdir(path.dirname(__dirname)); 16 | deleteSync('temp'); // In case last test run failed. 17 | fs.mkdirSync('temp'); 18 | }); 19 | 20 | test.after(() => { 21 | deleteSync('temp'); 22 | }); 23 | 24 | test.beforeEach(t => { 25 | t.context.source = path.join('temp', crypto.randomUUID()); 26 | t.context.destination = path.join('temp', crypto.randomUUID()); 27 | }); 28 | 29 | test('report progress', async t => { 30 | const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); 31 | fs.writeFileSync(t.context.source, buffer); 32 | 33 | let callCount = 0; 34 | 35 | await copyFile(t.context.source, t.context.destination, { 36 | onProgress(progress) { 37 | callCount++; 38 | t.is(typeof progress.sourcePath, 'string'); 39 | t.is(typeof progress.destinationPath, 'string'); 40 | t.is(typeof progress.size, 'number'); 41 | t.is(typeof progress.writtenBytes, 'number'); 42 | t.is(typeof progress.percent, 'number'); 43 | t.is(progress.size, THREE_HUNDRED_KILO); 44 | }, 45 | }); 46 | 47 | t.true(callCount > 0); 48 | }); 49 | 50 | test('report progress of 100% on end', async t => { 51 | const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); 52 | fs.writeFileSync(t.context.source, buffer); 53 | 54 | let lastRecord; 55 | 56 | await copyFile(t.context.source, t.context.destination, { 57 | onProgress(progress) { 58 | lastRecord = progress; 59 | }, 60 | }); 61 | 62 | t.is(lastRecord.percent, 1); 63 | t.is(lastRecord.writtenBytes, THREE_HUNDRED_KILO); 64 | }); 65 | 66 | test('report progress for empty files once', async t => { 67 | fs.writeFileSync(t.context.source, ''); 68 | 69 | let callCount = 0; 70 | 71 | await copyFile(t.context.source, t.context.destination, { 72 | onProgress(progress) { 73 | callCount++; 74 | t.is(progress.size, 0); 75 | t.is(progress.writtenBytes, 0); 76 | t.is(progress.percent, 1); 77 | }, 78 | }); 79 | 80 | t.is(callCount, 1); 81 | }); 82 | -------------------------------------------------------------------------------- /test/sync.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import crypto from 'node:crypto'; 3 | import {fileURLToPath} from 'node:url'; 4 | import path from 'node:path'; 5 | import fs from 'node:fs'; 6 | import {deleteSync} from 'del'; 7 | import test from 'ava'; 8 | import sinon from 'sinon'; 9 | import {copyFileSync} from '../index.js'; 10 | import assertDateEqual from './helpers/_assert.js'; 11 | import {buildEACCES, buildENOSPC, buildEBADF} from './helpers/_fs-errors.js'; 12 | 13 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 14 | 15 | const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; 16 | 17 | test.before(() => { 18 | process.chdir(path.dirname(__dirname)); 19 | deleteSync('temp'); // In case last test run failed. 20 | fs.mkdirSync('temp'); 21 | }); 22 | 23 | test.after(() => { 24 | deleteSync('temp'); 25 | }); 26 | 27 | test.beforeEach(t => { 28 | t.context.source = path.join('temp', crypto.randomUUID()); 29 | t.context.destination = path.join('temp', crypto.randomUUID()); 30 | }); 31 | 32 | test('throw an Error on missing `source`', t => { 33 | t.throws(() => { 34 | copyFileSync(); 35 | }, { 36 | message: /`source`/, 37 | }); 38 | }); 39 | 40 | test('throw an Error on missing `destination`', t => { 41 | t.throws(() => { 42 | copyFileSync('TARGET'); 43 | }, { 44 | message: /`destination`/, 45 | }); 46 | }); 47 | 48 | test('copy a file', t => { 49 | copyFileSync('license', t.context.destination); 50 | t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); 51 | }); 52 | 53 | test('copy an empty file', t => { 54 | fs.writeFileSync(t.context.source, ''); 55 | copyFileSync(t.context.source, t.context.destination); 56 | t.is(fs.readFileSync(t.context.destination, 'utf8'), ''); 57 | }); 58 | 59 | test('copy big files', t => { 60 | const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); 61 | fs.writeFileSync(t.context.source, buffer); 62 | copyFileSync(t.context.source, t.context.destination); 63 | t.true(buffer.equals(fs.readFileSync(t.context.destination))); 64 | }); 65 | 66 | test('do not alter overwrite option', t => { 67 | const options = {}; 68 | copyFileSync('license', t.context.destination, options); 69 | t.false('overwrite' in options); 70 | }); 71 | 72 | test('overwrite when enabled', t => { 73 | fs.writeFileSync(t.context.destination, ''); 74 | copyFileSync('license', t.context.destination, {overwrite: true}); 75 | t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); 76 | }); 77 | 78 | test('overwrite when options are undefined', t => { 79 | fs.writeFileSync(t.context.destination, ''); 80 | copyFileSync('license', t.context.destination); 81 | t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); 82 | }); 83 | 84 | test('do not overwrite when disabled', t => { 85 | fs.writeFileSync(t.context.destination, ''); 86 | 87 | const error = t.throws(() => { 88 | copyFileSync('license', t.context.destination, {overwrite: false}); 89 | }, { 90 | name: 'CopyFileError', 91 | }); 92 | 93 | t.is(error.code, 'EEXIST'); 94 | t.is(fs.readFileSync(t.context.destination, 'utf8'), ''); 95 | }); 96 | 97 | if (process.platform !== 'win32') { 98 | test('create directories with specified mode', t => { 99 | const directory = t.context.destination; 100 | const destination = `${directory}/${crypto.randomUUID()}`; 101 | const directoryMode = 0o700; 102 | copyFileSync('license', destination, {directoryMode}); 103 | const stat = fs.statSync(directory); 104 | t.is(stat.mode & directoryMode, directoryMode); // eslint-disable-line no-bitwise 105 | }); 106 | } 107 | 108 | test('do not create `destination` on unreadable `source`', t => { 109 | t.throws( 110 | () => { 111 | copyFileSync('node_modules', t.context.destination); 112 | }, 113 | { 114 | name: 'CopyFileError', 115 | code: 'EISDIR', 116 | }, 117 | ); 118 | 119 | t.throws(() => { 120 | fs.statSync(t.context.destination); 121 | }, { 122 | message: /ENOENT/, 123 | }); 124 | }); 125 | 126 | test('do not create `destination` directory on unreadable `source`', t => { 127 | t.throws( 128 | () => { 129 | copyFileSync('node_modules', `subdir/${crypto.randomUUID()}`); 130 | }, 131 | { 132 | name: 'CopyFileError', 133 | code: 'EISDIR', 134 | }, 135 | ); 136 | 137 | t.throws(() => { 138 | fs.statSync('subdir'); 139 | }, { 140 | message: /ENOENT/, 141 | }); 142 | }); 143 | 144 | test('preserve timestamps', t => { 145 | copyFileSync('license', t.context.destination); 146 | const licenseStats = fs.lstatSync('license'); 147 | const temporaryStats = fs.lstatSync(t.context.destination); 148 | assertDateEqual(t, licenseStats.atime, temporaryStats.atime); 149 | assertDateEqual(t, licenseStats.mtime, temporaryStats.mtime); 150 | }); 151 | 152 | test('preserve mode', t => { 153 | copyFileSync('license', t.context.destination); 154 | const licenseStats = fs.lstatSync('license'); 155 | const temporaryStats = fs.lstatSync(t.context.destination); 156 | t.is(licenseStats.mode, temporaryStats.mode); 157 | }); 158 | 159 | test('throw an Error if `source` does not exists', t => { 160 | const error = t.throws(() => { 161 | copyFileSync('NO_ENTRY', t.context.destination); 162 | }); 163 | t.is(error.name, 'CopyFileError', error.message); 164 | t.is(error.code, 'ENOENT', error.message); 165 | t.regex(error.message, /`NO_ENTRY`/, error.message); 166 | t.regex(error.stack, /`NO_ENTRY`/, error.message); 167 | }); 168 | 169 | test.failing('rethrow mkdir EACCES errors', t => { 170 | const directoryPath = `/root/NO_ACCESS_${crypto.randomUUID()}`; 171 | const destination = path.join(directoryPath, crypto.randomUUID()); 172 | const mkdirError = buildEACCES(directoryPath); 173 | 174 | fs.mkdirSync = sinon.stub(fs, 'mkdirSync').throws(mkdirError); 175 | 176 | const error = t.throws(() => { 177 | copyFileSync('license', destination); 178 | }); 179 | t.is(error.name, 'CopyFileError', error.message); 180 | t.is(error.errno, mkdirError.errno, error.message); 181 | t.is(error.code, mkdirError.code, error.message); 182 | t.is(error.path, mkdirError.path, error.message); 183 | t.true(fs.mkdirSync.called); 184 | 185 | fs.mkdirSync.restore(); 186 | }); 187 | 188 | test('rethrow ENOSPC errors', t => { 189 | const noSpaceError = buildENOSPC(); 190 | 191 | fs.writeFileSync(t.context.source, ''); 192 | fs.copyFileSync = sinon.stub(fs, 'copyFileSync').throws(noSpaceError); 193 | 194 | const error = t.throws(() => { 195 | copyFileSync('license', t.context.destination); 196 | }); 197 | t.is(error.name, 'CopyFileError', error.message); 198 | t.is(error.errno, noSpaceError.errno, error.message); 199 | t.is(error.code, noSpaceError.code, error.message); 200 | t.true(fs.copyFileSync.called); 201 | 202 | fs.copyFileSync.restore(); 203 | }); 204 | 205 | test.failing('rethrow stat errors', t => { 206 | const statError = buildEBADF(); 207 | 208 | fs.writeFileSync(t.context.source, ''); 209 | 210 | fs.statSync = sinon.stub(fs, 'statSync').throws(statError); 211 | 212 | const error = t.throws(() => { 213 | copyFileSync(t.context.source, t.context.destination); 214 | }); 215 | t.is(error.name, 'CopyFileError', error.message); 216 | t.is(error.errno, statError.errno, error.message); 217 | t.is(error.code, statError.code, error.message); 218 | t.true(fs.statSync.called); 219 | 220 | fs.statSync.restore(); 221 | }); 222 | 223 | test.failing('rethrow utimes errors', t => { 224 | const futimesError = buildEBADF(); 225 | 226 | fs.utimesSync = sinon.stub(fs, 'utimesSync').throws(futimesError); 227 | 228 | const error = t.throws(() => { 229 | copyFileSync('license', t.context.destination); 230 | }); 231 | t.is(error.name, 'CopyFileError', error.message); 232 | t.is(error.errno, futimesError.errno, error.message); 233 | t.is(error.code, futimesError.code, error.message); 234 | t.true(fs.utimesSync.called); 235 | 236 | fs.utimesSync.restore(); 237 | }); 238 | 239 | test('cwd option', t => { 240 | const error = t.throws(() => { 241 | copyFileSync('sync.js', t.context.destination); 242 | }); 243 | 244 | t.is(error.name, 'CopyFileError'); 245 | t.is(error.code, 'ENOENT'); 246 | 247 | t.notThrows(() => { 248 | copyFileSync('sync.js', t.context.destination, {cwd: 'test'}); 249 | }); 250 | }); 251 | --------------------------------------------------------------------------------