Repository: sindresorhus/cp-file Branch: main Commit: e08be4f61f6e Files: 19 Total size: 37.8 KB Directory structure: gitextract_2veczg2k/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── 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 ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] indent_style = tab end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_style = space indent_size = 2 ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf ================================================ FILE: .github/security.md ================================================ # Security Policy To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. ================================================ FILE: .github/workflows/main.yml ================================================ name: CI on: - push - pull_request jobs: test: name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: node-version: - 20 - 18 os: - ubuntu-latest - macos-latest - windows-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test ================================================ FILE: .gitignore ================================================ node_modules yarn.lock .nyc_output coverage temp ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: copy-file-error.js ================================================ export default class CopyFileError extends Error { constructor(message, {cause} = {}) { super(message, {cause}); Object.assign(this, cause); this.name = 'CopyFileError'; } } ================================================ FILE: fs.js ================================================ import {promisify} from 'node:util'; import fs from 'graceful-fs'; import {pEvent} from 'p-event'; import CopyFileError from './copy-file-error.js'; const statP = promisify(fs.stat); const lstatP = promisify(fs.lstat); const utimesP = promisify(fs.utimes); const chmodP = promisify(fs.chmod); const makeDirectoryP = promisify(fs.mkdir); export const closeSync = fs.closeSync.bind(fs); export const createWriteStream = fs.createWriteStream.bind(fs); export async function createReadStream(path, options) { const read = fs.createReadStream(path, options); try { await pEvent(read, ['readable', 'end']); } catch (error) { throw new CopyFileError(`Cannot read from \`${path}\`: ${error.message}`, {cause: error}); } return read; } export const stat = path => statP(path).catch(error => { throw new CopyFileError(`Cannot stat path \`${path}\`: ${error.message}`, {cause: error}); }); export const lstat = path => lstatP(path).catch(error => { throw new CopyFileError(`lstat \`${path}\` failed: ${error.message}`, {cause: error}); }); export const utimes = (path, atime, mtime) => utimesP(path, atime, mtime).catch(error => { throw new CopyFileError(`utimes \`${path}\` failed: ${error.message}`, {cause: error}); }); export const chmod = (path, mode) => chmodP(path, mode).catch(error => { throw new CopyFileError(`chmod \`${path}\` failed: ${error.message}`, {cause: error}); }); export const statSync = path => { try { return fs.statSync(path); } catch (error) { throw new CopyFileError(`stat \`${path}\` failed: ${error.message}`, {cause: error}); } }; export const lstatSync = path => { try { return fs.statSync(path); } catch (error) { throw new CopyFileError(`stat \`${path}\` failed: ${error.message}`, {cause: error}); } }; export const utimesSync = (path, atime, mtime) => { try { return fs.utimesSync(path, atime, mtime); } catch (error) { throw new CopyFileError(`utimes \`${path}\` failed: ${error.message}`, {cause: error}); } }; export const makeDirectory = (path, options) => makeDirectoryP(path, {...options, recursive: true}).catch(error => { throw new CopyFileError(`Cannot create directory \`${path}\`: ${error.message}`, {cause: error}); }); export const makeDirectorySync = (path, options) => { try { fs.mkdirSync(path, {...options, recursive: true}); } catch (error) { throw new CopyFileError(`Cannot create directory \`${path}\`: ${error.message}`, {cause: error}); } }; export const copyFileSync = (source, destination, flags) => { try { fs.copyFileSync(source, destination, flags); } catch (error) { throw new CopyFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, {cause: error}); } }; ================================================ FILE: index.d.ts ================================================ export type Options = { /** Overwrite existing destination file. @default true */ readonly overwrite?: boolean; /** [Permissions](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) for created directories. It has no effect on Windows. @default 0o777 */ readonly directoryMode?: number; /** The working directory to find source files. The source and destination path are relative to this. @default process.cwd() */ readonly cwd?: string; }; export type AsyncOptions = { /** The given function is called whenever there is measurable progress. Note: For empty files, the `onProgress` event is emitted only once. @example ``` import {copyFile} from 'copy-file'; await copyFile('source/unicorn.png', 'destination/unicorn.png', { onProgress: progress => { // … } }); ``` */ readonly onProgress?: (progress: ProgressData) => void; }; export type ProgressData = { /** Absolute path to source. */ sourcePath: string; /** Absolute path to destination. */ destinationPath: string; /** File size in bytes. */ size: number; /** Copied size in bytes. */ writtenBytes: number; /** Copied percentage, a value between `0` and `1`. */ percent: number; }; /** Copy a file. @param source - The file you want to copy. @param destination - Where you want the file copied. @returns A `Promise` that resolves when the file is copied. The file is cloned if the `onProgress` option is not passed and the [file system supports it](https://stackoverflow.com/a/76496347/64949). @example ``` import {copyFile} from 'copy-file'; await copyFile('source/unicorn.png', 'destination/unicorn.png'); console.log('File copied'); ``` */ export function copyFile(source: string, destination: string, options?: Options & AsyncOptions): Promise; /** Copy a file synchronously. @param source - The file you want to copy. @param destination - Where you want the file copied. The file is cloned if the [file system supports it](https://stackoverflow.com/a/76496347/64949). @example ``` import {copyFileSync} from 'copy-file'; copyFileSync('source/unicorn.png', 'destination/unicorn.png'); ``` */ export function copyFileSync(source: string, destination: string, options?: Options): void; ================================================ FILE: index.js ================================================ import path from 'node:path'; import realFS, {constants as fsConstants} from 'node:fs'; import realFSPromises from 'node:fs/promises'; import {pEvent} from 'p-event'; import CopyFileError from './copy-file-error.js'; import * as fs from './fs.js'; const resolvePath = (cwd, sourcePath, destinationPath) => ({ sourcePath: path.resolve(cwd, sourcePath), destinationPath: path.resolve(cwd, destinationPath), }); const checkSourceIsFile = (stat, source) => { if (!stat.isFile()) { throw Object.assign(new CopyFileError(`EISDIR: illegal operation on a directory '${source}'`), { errno: -21, code: 'EISDIR', source, }); } }; export async function copyFile(sourcePath, destinationPath, options = {}) { if (!sourcePath || !destinationPath) { throw new CopyFileError('`source` and `destination` required'); } if (options.cwd) { ({sourcePath, destinationPath} = resolvePath(options.cwd, sourcePath, destinationPath)); } options = { overwrite: true, ...options, }; const stats = await fs.lstat(sourcePath); const {size} = stats; checkSourceIsFile(stats, sourcePath); const destinationDirectory = path.dirname(destinationPath); const isRootDirectory = path.parse(destinationDirectory).root === destinationDirectory; if (!isRootDirectory) { await fs.makeDirectory(path.dirname(destinationPath), {mode: options.directoryMode}); } if (typeof options.onProgress === 'function') { const readStream = await fs.createReadStream(sourcePath); const writeStream = fs.createWriteStream(destinationPath, {flags: options.overwrite ? 'w' : 'wx'}); const emitProgress = writtenBytes => { options.onProgress({ sourcePath: path.resolve(sourcePath), destinationPath: path.resolve(destinationPath), size, writtenBytes, percent: writtenBytes === size ? 1 : writtenBytes / size, }); }; let lastEmittedPercent = -1; readStream.on('data', () => { const written = writeStream.bytesWritten; const percent = written === size ? 1 : written / size; // Throttle progress events. if (percent === 1 || (percent - lastEmittedPercent) >= 0.01) { lastEmittedPercent = percent; emitProgress(written); } }); let readError; readStream.once('error', error => { readError = new CopyFileError(`Cannot read from \`${sourcePath}\`: ${error.message}`, {cause: error}); }); let shouldUpdateStats = false; try { const writePromise = pEvent(writeStream, 'close'); readStream.pipe(writeStream); await writePromise; emitProgress(size); shouldUpdateStats = true; } catch (error) { throw new CopyFileError(`Cannot write to \`${destinationPath}\`: ${error.message}`, {cause: error}); } if (readError) { throw readError; } if (shouldUpdateStats) { const stats = await fs.lstat(sourcePath); return Promise.all([ fs.utimes(destinationPath, stats.atime, stats.mtime).catch(error => { // Ignore EPERM errors on utime (e.g., Samba shares) if (error.code !== 'EPERM') { throw error; } }), fs.chmod(destinationPath, stats.mode), ]); } } else { // eslint-disable-next-line no-bitwise const flags = options.overwrite ? fsConstants.COPYFILE_FICLONE : (fsConstants.COPYFILE_FICLONE | fsConstants.COPYFILE_EXCL); try { await realFSPromises.copyFile(sourcePath, destinationPath, flags); await Promise.all([ realFSPromises.utimes(destinationPath, stats.atime, stats.mtime).catch(error => { // Ignore EPERM errors on utime (e.g., Samba shares) if (error.code !== 'EPERM') { throw error; } }), realFSPromises.chmod(destinationPath, stats.mode), ]); } catch (error) { throw new CopyFileError(error.message, {cause: error}); } } } export function copyFileSync(sourcePath, destinationPath, options = {}) { if (!sourcePath || !destinationPath) { throw new CopyFileError('`source` and `destination` required'); } if (options.cwd) { ({sourcePath, destinationPath} = resolvePath(options.cwd, sourcePath, destinationPath)); } options = { overwrite: true, ...options, }; const stats = fs.lstatSync(sourcePath); checkSourceIsFile(stats, sourcePath); const destinationDirectory = path.dirname(destinationPath); const isRootDirectory = path.parse(destinationDirectory).root === destinationDirectory; if (!isRootDirectory) { fs.makeDirectorySync(path.dirname(destinationPath), {mode: options.directoryMode}); } // eslint-disable-next-line no-bitwise const flags = options.overwrite ? fsConstants.COPYFILE_FICLONE : (fsConstants.COPYFILE_FICLONE | fsConstants.COPYFILE_EXCL); try { realFS.copyFileSync(sourcePath, destinationPath, flags); } catch (error) { throw new CopyFileError(error.message, {cause: error}); } try { fs.utimesSync(destinationPath, stats.atime, stats.mtime); } catch (error) { // Ignore EPERM errors on utime (e.g., Samba shares) if (error.code !== 'EPERM') { throw error; } } fs.chmod(destinationPath, stats.mode); } ================================================ FILE: index.test-d.ts ================================================ import {expectError, expectType} from 'tsd'; import {copyFile, copyFileSync, type ProgressData} from './index.js'; expectType >( copyFile('source/unicorn.png', 'destination/unicorn.png'), ); expectType>( copyFile('source/unicorn.png', 'destination/unicorn.png', {overwrite: false}), ); expectType>( copyFile('source/unicorn.png', 'destination/unicorn.png', { directoryMode: 0o700, }), ); expectError( await copyFile('source/unicorn.png', 'destination/unicorn.png', { directoryMode: '700', }), ); expectType>( copyFile('source/unicorn.png', 'destination/unicorn.png', { onProgress(progress) { expectType(progress); expectType(progress.sourcePath); expectType(progress.destinationPath); expectType(progress.size); expectType(progress.writtenBytes); expectType(progress.percent); }, }), ); expectType(copyFileSync('source/unicorn.png', 'destination/unicorn.png')); expectType( copyFileSync('source/unicorn.png', 'destination/unicorn.png', { overwrite: false, }), ); expectType( copyFileSync('source/unicorn.png', 'destination/unicorn.png', { directoryMode: 0o700, }), ); expectError( copyFileSync('source/unicorn.png', 'destination/unicorn.png', { directoryMode: '700', }), ); ================================================ FILE: license ================================================ MIT License Copyright (c) Sindre Sorhus (https://sindresorhus.com) 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. ================================================ FILE: package.json ================================================ { "name": "copy-file", "version": "11.1.0", "description": "Copy a file", "license": "MIT", "repository": "sindresorhus/copy-file", "funding": "https://github.com/sponsors/sindresorhus", "author": { "name": "Sindre Sorhus", "email": "sindresorhus@gmail.com", "url": "https://sindresorhus.com" }, "type": "module", "exports": { "types": "./index.d.ts", "default": "./index.js" }, "sideEffects": false, "engines": { "node": ">=18" }, "scripts": { "test": "xo && nyc ava && tsd" }, "files": [ "index.js", "index.d.ts", "copy-file-error.js", "fs.js" ], "keywords": [ "copy", "copying", "cp", "file", "clone", "fs", "stream", "file-system", "filesystem", "ncp", "fast", "quick", "data", "content", "contents", "read", "write", "io" ], "dependencies": { "graceful-fs": "^4.2.11", "p-event": "^6.0.0" }, "devDependencies": { "ava": "^5.3.1", "clear-module": "^4.1.2", "coveralls": "^3.1.1", "del": "^7.1.0", "import-fresh": "^3.3.0", "nyc": "^15.1.0", "sinon": "^17.0.1", "tsd": "^0.29.0", "xo": "^0.56.0" }, "xo": { "rules": { "ava/assertion-arguments": "off" } }, "ava": { "workerThreads": false, "serial": true } } ================================================ FILE: readme.md ================================================ # copy-file > Copy a file ## Highlights - 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. - Resilient by using [graceful-fs](https://github.com/isaacs/node-graceful-fs). - User-friendly by creating non-existent destination directories for you. - Can be safe by turning off [overwriting](#overwrite). - Preserves file mode [but not ownership](https://github.com/sindresorhus/copy-file/issues/22#issuecomment-502079547). - User-friendly errors. ## Install ```sh npm install copy-file ``` ## Usage ```js import {copyFile} from 'copy-file'; await copyFile('source/unicorn.png', 'destination/unicorn.png'); console.log('File copied'); ``` ## API ### copyFile(source, destination, options?) Returns a `Promise` that resolves when the file is copied. The file is cloned if the `onProgress` option is not passed and the [file system supports it](https://stackoverflow.com/a/76496347/64949). ### copyFileSync(source, destination, options?) #### source Type: `string` The file you want to copy. The file is cloned if the [file system supports it](https://stackoverflow.com/a/76496347/64949). #### destination Type: `string` Where you want the file copied. #### options Type: `object` ##### overwrite Type: `boolean`\ Default: `true` Overwrite existing destination file. ##### cwd Type: `string`\ Default: `process.cwd()` The working directory to find source files. The source and destination path are relative to this. ##### directoryMode Type: `number`\ Default: `0o777` [Permissions](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) for created directories. It has no effect on Windows. ##### onProgress Type: `(progress: ProgressData) => void` The given function is called whenever there is measurable progress. Only available when using the async method. ###### `ProgressData` ```js { sourcePath: string, destinationPath: string, size: number, writtenBytes: number, percent: number } ``` - `sourcePath` and `destinationPath` are absolute paths. - `size` and `writtenBytes` are in bytes. - `percent` is a value between `0` and `1`. ###### Notes - For empty files, the `onProgress` callback function is emitted only once. ```js import {copyFile} from 'copy-file'; await copyFile(source, destination, { onProgress: progress => { // … } }); ``` ## Related - [cpy](https://github.com/sindresorhus/cpy) - Copy files - [cpy-cli](https://github.com/sindresorhus/cpy-cli) - Copy files on the command-line - [move-file](https://github.com/sindresorhus/move-file) - Move a file - [make-dir](https://github.com/sindresorhus/make-dir) - Make a directory and its parents if needed ================================================ FILE: test/async.js ================================================ import process from 'node:process'; import crypto from 'node:crypto'; import path from 'node:path'; import fs from 'node:fs'; import {fileURLToPath} from 'node:url'; import importFresh from 'import-fresh'; import clearModule from 'clear-module'; import {deleteSync} from 'del'; import test from 'ava'; import sinon from 'sinon'; import {copyFile} from '../index.js'; import assertDateEqual from './helpers/_assert.js'; import {buildEACCES, buildENOSPC, buildENOENT, buildEPERM, buildERRSTREAMWRITEAFTEREND} from './helpers/_fs-errors.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; test.before(() => { process.chdir(path.dirname(__dirname)); deleteSync('temp'); // In case last test run failed. fs.mkdirSync('temp'); }); test.after(() => { deleteSync('temp'); }); test.beforeEach(t => { t.context.source = path.join('temp', crypto.randomUUID()); t.context.destination = path.join('temp', crypto.randomUUID()); }); test('reject an Error on missing `source`', async t => { await t.throwsAsync(copyFile(), { message: /`source`/, }); }); test('reject an Error on missing `destination`', async t => { await t.throwsAsync(copyFile('TARGET'), { message: /`destination`/, }); }); test('copy a file', async t => { await copyFile('license', t.context.destination); t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('copy an empty file', async t => { fs.writeFileSync(t.context.source, ''); await copyFile(t.context.source, t.context.destination); t.is(fs.readFileSync(t.context.destination, 'utf8'), ''); }); test('copy big files', async t => { const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); fs.writeFileSync(t.context.source, buffer); await copyFile(t.context.source, t.context.destination); t.true(buffer.equals(fs.readFileSync(t.context.destination))); }); test('do not alter overwrite option', async t => { const options = {}; await copyFile('license', t.context.destination, options); t.false('overwrite' in options); }); test('overwrite when enabled', async t => { fs.writeFileSync(t.context.destination, ''); await copyFile('license', t.context.destination, {overwrite: true}); t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('overwrite when options are undefined', async t => { fs.writeFileSync(t.context.destination, ''); await copyFile('license', t.context.destination); t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('do not overwrite when disabled', async t => { fs.writeFileSync(t.context.destination, ''); const error = await t.throwsAsync(copyFile('license', t.context.destination, {overwrite: false})); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, 'EEXIST', error.message); }); if (process.platform !== 'win32') { test('create directories with specified mode', async t => { const directory = t.context.destination; const destination = `${directory}/${crypto.randomUUID()}`; const directoryMode = 0o700; await copyFile('license', destination, {directoryMode}); const stat = fs.statSync(directory); t.is(stat.mode & directoryMode, directoryMode); // eslint-disable-line no-bitwise }); } test('do not create `destination` on unreadable `source`', async t => { const error = await t.throwsAsync(copyFile('node_modules', t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, 'EISDIR', error.message); t.throws(() => { fs.statSync(t.context.destination); }, { message: /ENOENT/, }); }); test('do not create `destination` directory on unreadable `source`', async t => { const error = await t.throwsAsync(copyFile('node_modules', path.join('temp/subdir', crypto.randomUUID()))); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, 'EISDIR', error.message); t.false(fs.existsSync('subdir')); }); test('preserve timestamps', async t => { await copyFile('license', t.context.destination); const licenseStats = fs.lstatSync('license'); const temporaryStats = fs.lstatSync(t.context.destination); assertDateEqual(t, licenseStats.atime, temporaryStats.atime); assertDateEqual(t, licenseStats.mtime, temporaryStats.mtime); }); test('preserve mode', async t => { await copyFile('license', t.context.destination); const licenseStats = fs.lstatSync('license'); const temporaryStats = fs.lstatSync(t.context.destination); t.is(licenseStats.mode, temporaryStats.mode); }); test('throw an Error if `source` does not exists', async t => { const error = await t.throwsAsync(copyFile('NO_ENTRY', t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, 'ENOENT', error.message); t.regex(error.message, /`NO_ENTRY`/, error.message); t.regex(error.stack, /`NO_ENTRY`/, error.message); }); test.serial.failing('rethrow mkdir EACCES errors', async t => { const directoryPath = `/root/NO_ACCESS_${crypto.randomUUID()}`; const destination = path.join(directoryPath, crypto.randomUUID()); const mkdirError = buildEACCES(directoryPath); fs.stat = sinon.stub(fs, 'stat').throws(mkdirError); fs.mkdir = sinon.stub(fs, 'mkdir').throws(mkdirError); const error = await t.throwsAsync(copyFile('license', destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, mkdirError.errno, error.message); t.is(error.code, mkdirError.code, error.message); t.is(error.path, mkdirError.path, error.message); t.true(fs.mkdir.called || fs.stat.called); fs.mkdir.restore(); fs.stat.restore(); }); test.serial.failing('rethrow ENOSPC errors', async t => { const {createWriteStream} = fs; const noSpaceError = buildENOSPC(); let isCalled = false; fs.createWriteStream = (path, options) => { const stream = createWriteStream(path, options); if (path === t.context.destination) { stream.on('pipe', () => { if (!isCalled) { isCalled = true; stream.emit('error', noSpaceError); } }); } return stream; }; clearModule('../fs.js'); const uncached = importFresh('../index.js'); const error = await t.throwsAsync(uncached('license', t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, noSpaceError.errno, error.message); t.is(error.code, noSpaceError.code, error.message); t.true(isCalled); fs.createWriteStream = createWriteStream; }); test.serial.failing('rethrow stat errors', async t => { const fstatError = buildENOENT(); fs.writeFileSync(t.context.source, ''); fs.lstat = sinon.stub(fs, 'lstat').throws(fstatError); clearModule('../fs.js'); const uncached = importFresh('../index.js'); const error = await t.throwsAsync(uncached(t.context.source, t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, fstatError.errno, error.message); t.is(error.code, fstatError.code, error.message); t.true(fs.lstat.called); fs.lstat.restore(); }); test.serial.failing('rethrow utimes errors', async t => { const utimesError = buildENOENT(); fs.utimes = sinon.stub(fs, 'utimes').throws(utimesError); clearModule('../fs.js'); const uncached = importFresh('../index.js'); const error = await t.throwsAsync(uncached('license', t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, 'ENOENT', error.message); t.true(fs.utimes.called); fs.utimes.restore(); }); test.serial.failing('rethrow chmod errors', async t => { const chmodError = buildEPERM(t.context.destination, 'chmod'); fs.chmod = sinon.stub(fs, 'chmod').throws(chmodError); clearModule('../fs.js'); const uncached = importFresh('../index.js'); const error = await t.throwsAsync(uncached('license', t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, chmodError.code, error.message); t.is(error.path, chmodError.path, error.message); t.true(fs.chmod.called); fs.chmod.restore(); }); test.serial.failing('rethrow read after open errors', async t => { const {createWriteStream, createReadStream} = fs; let calledWriteEnd = 0; let readStream; const readError = buildERRSTREAMWRITEAFTEREND(); fs.createWriteStream = (...arguments_) => { const stream = createWriteStream(...arguments_); const {end} = stream; stream.on('pipe', () => { readStream.emit('error', readError); }); stream.end = (...endArgs) => { calledWriteEnd++; return end.apply(stream, endArgs); }; return stream; }; fs.createReadStream = (...arguments_) => { /* Fake stream */ readStream = createReadStream(...arguments_); readStream.pause(); return readStream; }; clearModule('../fs.js'); const uncached = importFresh('../index.js'); const error = await t.throwsAsync(uncached('license', t.context.destination)); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, readError.code, error.message); t.is(error.errno, readError.errno, error.message); t.is(calledWriteEnd, 1); Object.assign(fs, {createWriteStream, createReadStream}); }); test('cwd option', async t => { const error = await t.throwsAsync(copyFile('sync.js', t.context.destination)); t.is(error.name, 'CopyFileError'); t.is(error.code, 'ENOENT'); await t.notThrowsAsync(copyFile('sync.js', t.context.destination, {cwd: 'test'})); }); ================================================ FILE: test/helpers/_assert.js ================================================ /** Tests equality of Date objects, w/o considering milliseconds. @see {@link https://github.com/joyent/node/issues/7000|File timestamp resolution is inconsistent with fs.stat / fs.utimes} @param {Object} t - AVA's t @param {*} actual - the actual value @param {*} expected - the expected value @param {*} message - error message */ export default function assertDateEqual(t, actual, expected, message) { actual = new Date(actual); expected = new Date(expected); actual.setMilliseconds(0); expected.setMilliseconds(0); t.is(actual.getTime(), expected.getTime(), message); } ================================================ FILE: test/helpers/_fs-errors.js ================================================ export const buildEACCES = path => Object.assign(new Error(`EACCES: permission denied '${path}'`), { errno: -13, code: 'EACCES', path, }); export const buildENOSPC = () => Object.assign(new Error('ENOSPC, write'), { errno: -28, code: 'ENOSPC', }); export const buildENOENT = path => Object.assign(new Error(`ENOENT: no such file or directory '${path}'`), { errno: -2, code: 'ENOENT', path, }); export const buildERRSTREAMWRITEAFTEREND = () => Object.assign(new Error('ERR_STREAM_WRITE_AFTER_END'), { code: 'ERR_STREAM_WRITE_AFTER_END', }); export const buildEBADF = () => Object.assign(new Error('EBADF: bad file descriptor'), { errno: -9, code: 'EBADF', }); export const buildEPERM = (path, method) => Object.assign(new Error(`EPERM: ${method} '${path}''`), { errno: 50, code: 'EPERM', }); ================================================ FILE: test/progress.js ================================================ import {Buffer} from 'node:buffer'; import process from 'node:process'; import crypto from 'node:crypto'; import path from 'node:path'; import {fileURLToPath} from 'node:url'; import fs from 'node:fs'; import {deleteSync} from 'del'; import test from 'ava'; import {copyFile} from '../index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; test.before(() => { process.chdir(path.dirname(__dirname)); deleteSync('temp'); // In case last test run failed. fs.mkdirSync('temp'); }); test.after(() => { deleteSync('temp'); }); test.beforeEach(t => { t.context.source = path.join('temp', crypto.randomUUID()); t.context.destination = path.join('temp', crypto.randomUUID()); }); test('report progress', async t => { const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); fs.writeFileSync(t.context.source, buffer); let callCount = 0; await copyFile(t.context.source, t.context.destination, { onProgress(progress) { callCount++; t.is(typeof progress.sourcePath, 'string'); t.is(typeof progress.destinationPath, 'string'); t.is(typeof progress.size, 'number'); t.is(typeof progress.writtenBytes, 'number'); t.is(typeof progress.percent, 'number'); t.is(progress.size, THREE_HUNDRED_KILO); }, }); t.true(callCount > 0); }); test('report progress of 100% on end', async t => { const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); fs.writeFileSync(t.context.source, buffer); let lastRecord; await copyFile(t.context.source, t.context.destination, { onProgress(progress) { lastRecord = progress; }, }); t.is(lastRecord.percent, 1); t.is(lastRecord.writtenBytes, THREE_HUNDRED_KILO); }); test('report progress for empty files once', async t => { fs.writeFileSync(t.context.source, ''); let callCount = 0; await copyFile(t.context.source, t.context.destination, { onProgress(progress) { callCount++; t.is(progress.size, 0); t.is(progress.writtenBytes, 0); t.is(progress.percent, 1); }, }); t.is(callCount, 1); }); test('progress emits ≤101 times and finishes at 100%', async t => { const size = 10 * 1024 * 1024; // 10 MiB const source = path.join('temp', `cap-${crypto.randomUUID()}.bin`); const destination = path.join('temp', `cap-out-${crypto.randomUUID()}.bin`); fs.mkdirSync('temp', {recursive: true}); fs.writeFileSync(source, crypto.randomBytes(size)); let callCount = 0; let lastRecord; await copyFile(source, destination, { onProgress(progress) { callCount++; lastRecord = progress; }, }); t.true(callCount <= 101); t.is(lastRecord.percent, 1); t.is(lastRecord.writtenBytes, size); t.true(fs.existsSync(destination)); }); test('progress emits at least once for tiny/empty files', async t => { for (const bytes of [0, 1, 42]) { const source = path.join('temp', `tiny-${bytes}-${crypto.randomUUID()}`); const destination = path.join('temp', `tiny-out-${bytes}-${crypto.randomUUID()}`); fs.writeFileSync(source, Buffer.alloc(bytes)); let callCount = 0; let lastRecord; // eslint-disable-next-line no-await-in-loop await copyFile(source, destination, { onProgress(progress) { callCount++; lastRecord = progress; }, }); t.true(callCount >= 1); t.is(lastRecord.percent, 1); t.is(lastRecord.writtenBytes, bytes); } }); ================================================ FILE: test/sync.js ================================================ import process from 'node:process'; import crypto from 'node:crypto'; import {fileURLToPath} from 'node:url'; import path from 'node:path'; import fs from 'node:fs'; import {deleteSync} from 'del'; import test from 'ava'; import sinon from 'sinon'; import {copyFileSync} from '../index.js'; import assertDateEqual from './helpers/_assert.js'; import {buildEACCES, buildENOSPC, buildEBADF} from './helpers/_fs-errors.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1; test.before(() => { process.chdir(path.dirname(__dirname)); deleteSync('temp'); // In case last test run failed. fs.mkdirSync('temp'); }); test.after(() => { deleteSync('temp'); }); test.beforeEach(t => { t.context.source = path.join('temp', crypto.randomUUID()); t.context.destination = path.join('temp', crypto.randomUUID()); }); test('throw an Error on missing `source`', t => { t.throws(() => { copyFileSync(); }, { message: /`source`/, }); }); test('throw an Error on missing `destination`', t => { t.throws(() => { copyFileSync('TARGET'); }, { message: /`destination`/, }); }); test('copy a file', t => { copyFileSync('license', t.context.destination); t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('copy an empty file', t => { fs.writeFileSync(t.context.source, ''); copyFileSync(t.context.source, t.context.destination); t.is(fs.readFileSync(t.context.destination, 'utf8'), ''); }); test('copy big files', t => { const buffer = crypto.randomBytes(THREE_HUNDRED_KILO); fs.writeFileSync(t.context.source, buffer); copyFileSync(t.context.source, t.context.destination); t.true(buffer.equals(fs.readFileSync(t.context.destination))); }); test('do not alter overwrite option', t => { const options = {}; copyFileSync('license', t.context.destination, options); t.false('overwrite' in options); }); test('overwrite when enabled', t => { fs.writeFileSync(t.context.destination, ''); copyFileSync('license', t.context.destination, {overwrite: true}); t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('overwrite when options are undefined', t => { fs.writeFileSync(t.context.destination, ''); copyFileSync('license', t.context.destination); t.is(fs.readFileSync(t.context.destination, 'utf8'), fs.readFileSync('license', 'utf8')); }); test('do not overwrite when disabled', t => { fs.writeFileSync(t.context.destination, ''); const error = t.throws(() => { copyFileSync('license', t.context.destination, {overwrite: false}); }, { name: 'CopyFileError', }); t.is(error.code, 'EEXIST'); t.is(fs.readFileSync(t.context.destination, 'utf8'), ''); }); if (process.platform !== 'win32') { test('create directories with specified mode', t => { const directory = t.context.destination; const destination = `${directory}/${crypto.randomUUID()}`; const directoryMode = 0o700; copyFileSync('license', destination, {directoryMode}); const stat = fs.statSync(directory); t.is(stat.mode & directoryMode, directoryMode); // eslint-disable-line no-bitwise }); } test('do not create `destination` on unreadable `source`', t => { t.throws( () => { copyFileSync('node_modules', t.context.destination); }, { name: 'CopyFileError', code: 'EISDIR', }, ); t.throws(() => { fs.statSync(t.context.destination); }, { message: /ENOENT/, }); }); test('do not create `destination` directory on unreadable `source`', t => { t.throws( () => { copyFileSync('node_modules', `subdir/${crypto.randomUUID()}`); }, { name: 'CopyFileError', code: 'EISDIR', }, ); t.throws(() => { fs.statSync('subdir'); }, { message: /ENOENT/, }); }); test('preserve timestamps', t => { copyFileSync('license', t.context.destination); const licenseStats = fs.lstatSync('license'); const temporaryStats = fs.lstatSync(t.context.destination); assertDateEqual(t, licenseStats.atime, temporaryStats.atime); assertDateEqual(t, licenseStats.mtime, temporaryStats.mtime); }); test('preserve mode', t => { copyFileSync('license', t.context.destination); const licenseStats = fs.lstatSync('license'); const temporaryStats = fs.lstatSync(t.context.destination); t.is(licenseStats.mode, temporaryStats.mode); }); test('throw an Error if `source` does not exists', t => { const error = t.throws(() => { copyFileSync('NO_ENTRY', t.context.destination); }); t.is(error.name, 'CopyFileError', error.message); t.is(error.code, 'ENOENT', error.message); t.regex(error.message, /`NO_ENTRY`/, error.message); t.regex(error.stack, /`NO_ENTRY`/, error.message); }); test.failing('rethrow mkdir EACCES errors', t => { const directoryPath = `/root/NO_ACCESS_${crypto.randomUUID()}`; const destination = path.join(directoryPath, crypto.randomUUID()); const mkdirError = buildEACCES(directoryPath); fs.mkdirSync = sinon.stub(fs, 'mkdirSync').throws(mkdirError); const error = t.throws(() => { copyFileSync('license', destination); }); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, mkdirError.errno, error.message); t.is(error.code, mkdirError.code, error.message); t.is(error.path, mkdirError.path, error.message); t.true(fs.mkdirSync.called); fs.mkdirSync.restore(); }); test('rethrow ENOSPC errors', t => { const noSpaceError = buildENOSPC(); fs.writeFileSync(t.context.source, ''); fs.copyFileSync = sinon.stub(fs, 'copyFileSync').throws(noSpaceError); const error = t.throws(() => { copyFileSync('license', t.context.destination); }); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, noSpaceError.errno, error.message); t.is(error.code, noSpaceError.code, error.message); t.true(fs.copyFileSync.called); fs.copyFileSync.restore(); }); test.failing('rethrow stat errors', t => { const statError = buildEBADF(); fs.writeFileSync(t.context.source, ''); fs.statSync = sinon.stub(fs, 'statSync').throws(statError); const error = t.throws(() => { copyFileSync(t.context.source, t.context.destination); }); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, statError.errno, error.message); t.is(error.code, statError.code, error.message); t.true(fs.statSync.called); fs.statSync.restore(); }); test.failing('rethrow utimes errors', t => { const futimesError = buildEBADF(); fs.utimesSync = sinon.stub(fs, 'utimesSync').throws(futimesError); const error = t.throws(() => { copyFileSync('license', t.context.destination); }); t.is(error.name, 'CopyFileError', error.message); t.is(error.errno, futimesError.errno, error.message); t.is(error.code, futimesError.code, error.message); t.true(fs.utimesSync.called); fs.utimesSync.restore(); }); test('cwd option', t => { const error = t.throws(() => { copyFileSync('sync.js', t.context.destination); }); t.is(error.name, 'CopyFileError'); t.is(error.code, 'ENOENT'); t.notThrows(() => { copyFileSync('sync.js', t.context.destination, {cwd: 'test'}); }); });