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<void>;
/**
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<Promise<void> >(
copyFile('source/unicorn.png', 'destination/unicorn.png'),
);
expectType<Promise<void>>(
copyFile('source/unicorn.png', 'destination/unicorn.png', {overwrite: false}),
);
expectType<Promise<void>>(
copyFile('source/unicorn.png', 'destination/unicorn.png', {
directoryMode: 0o700,
}),
);
expectError(
await copyFile('source/unicorn.png', 'destination/unicorn.png', {
directoryMode: '700',
}),
);
expectType<Promise<void>>(
copyFile('source/unicorn.png', 'destination/unicorn.png', {
onProgress(progress) {
expectType<ProgressData>(progress);
expectType<string>(progress.sourcePath);
expectType<string>(progress.destinationPath);
expectType<number>(progress.size);
expectType<number>(progress.writtenBytes);
expectType<number>(progress.percent);
},
}),
);
expectType<void>(copyFileSync('source/unicorn.png', 'destination/unicorn.png'));
expectType<void>(
copyFileSync('source/unicorn.png', 'destination/unicorn.png', {
overwrite: false,
}),
);
expectType<void>(
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 <sindresorhus@gmail.com> (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'});
});
});
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
SYMBOL INDEX (18 symbols across 9 files)
FILE: copy-file-error.js
class CopyFileError (line 1) | class CopyFileError extends Error {
method constructor (line 2) | constructor(message, {cause} = {}) {
FILE: fs.js
function createReadStream (line 15) | async function createReadStream(path, options) {
FILE: index.d.ts
type Options (line 1) | type Options = {
type AsyncOptions (line 28) | type AsyncOptions = {
type ProgressData (line 48) | type ProgressData = {
FILE: index.js
function copyFile (line 23) | async function copyFile(sourcePath, destinationPath, options = {}) {
function copyFileSync (line 130) | function copyFileSync(sourcePath, destinationPath, options = {}) {
FILE: index.test-d.ts
method onProgress (line 22) | onProgress(progress) {
FILE: test/async.js
constant THREE_HUNDRED_KILO (line 17) | const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1;
FILE: test/helpers/_assert.js
function assertDateEqual (line 11) | function assertDateEqual(t, actual, expected, message) {
FILE: test/progress.js
constant THREE_HUNDRED_KILO (line 13) | const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1;
method onProgress (line 37) | onProgress(progress) {
method onProgress (line 58) | onProgress(progress) {
method onProgress (line 73) | onProgress(progress) {
method onProgress (line 94) | onProgress(progress) {
method onProgress (line 117) | onProgress(progress) {
FILE: test/sync.js
constant THREE_HUNDRED_KILO (line 15) | const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1;
Condensed preview — 19 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (42K chars).
[
{
"path": ".editorconfig",
"chars": 175,
"preview": "root = true\n\n[*]\nindent_style = tab\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newlin"
},
{
"path": ".gitattributes",
"chars": 19,
"preview": "* text=auto eol=lf\n"
},
{
"path": ".github/security.md",
"chars": 179,
"preview": "# Security Policy\n\nTo report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/s"
},
{
"path": ".github/workflows/main.yml",
"chars": 534,
"preview": "name: CI\non:\n - push\n - pull_request\njobs:\n test:\n name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }}\n "
},
{
"path": ".gitignore",
"chars": 49,
"preview": "node_modules\nyarn.lock\n.nyc_output\ncoverage\ntemp\n"
},
{
"path": ".npmrc",
"chars": 19,
"preview": "package-lock=false\n"
},
{
"path": "copy-file-error.js",
"chars": 182,
"preview": "export default class CopyFileError extends Error {\n\tconstructor(message, {cause} = {}) {\n\t\tsuper(message, {cause});\n\t\tOb"
},
{
"path": "fs.js",
"chars": 2704,
"preview": "import {promisify} from 'node:util';\nimport fs from 'graceful-fs';\nimport {pEvent} from 'p-event';\nimport CopyFileError "
},
{
"path": "index.d.ts",
"chars": 2251,
"preview": "export type Options = {\n\t/**\n\tOverwrite existing destination file.\n\n\t@default true\n\t*/\n\treadonly overwrite?: boolean;\n\n\t"
},
{
"path": "index.js",
"chars": 4967,
"preview": "import path from 'node:path';\nimport realFS, {constants as fsConstants} from 'node:fs';\nimport realFSPromises from 'node"
},
{
"path": "index.test-d.ts",
"chars": 1350,
"preview": "import {expectError, expectType} from 'tsd';\nimport {copyFile, copyFileSync, type ProgressData} from './index.js';\n\nexpe"
},
{
"path": "license",
"chars": 1117,
"preview": "MIT License\n\nCopyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)\n\nPermission is hereby grant"
},
{
"path": "package.json",
"chars": 1230,
"preview": "{\n\t\"name\": \"copy-file\",\n\t\"version\": \"11.1.0\",\n\t\"description\": \"Copy a file\",\n\t\"license\": \"MIT\",\n\t\"repository\": \"sindreso"
},
{
"path": "readme.md",
"chars": 2772,
"preview": "# copy-file\n\n> Copy a file\n\n## Highlights\n\n- It's super fast by [cloning](https://stackoverflow.com/questions/71629903/n"
},
{
"path": "test/async.js",
"chars": 9357,
"preview": "import process from 'node:process';\nimport crypto from 'node:crypto';\nimport path from 'node:path';\nimport fs from 'node"
},
{
"path": "test/helpers/_assert.js",
"chars": 583,
"preview": "/**\nTests equality of Date objects, w/o considering milliseconds.\n\n@see {@link https://github.com/joyent/node/issues/700"
},
{
"path": "test/helpers/_fs-errors.js",
"chars": 809,
"preview": "export const buildEACCES = path => Object.assign(new Error(`EACCES: permission denied '${path}'`), {\n\terrno: -13,\n\tcode:"
},
{
"path": "test/progress.js",
"chars": 3335,
"preview": "import {Buffer} from 'node:buffer';\nimport process from 'node:process';\nimport crypto from 'node:crypto';\nimport path fr"
},
{
"path": "test/sync.js",
"chars": 7048,
"preview": "import process from 'node:process';\nimport crypto from 'node:crypto';\nimport {fileURLToPath} from 'node:url';\nimport pat"
}
]
About this extraction
This page contains the full source code of the sindresorhus/cp-file GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 19 files (37.8 KB), approximately 10.4k tokens, and a symbol index with 18 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.