Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: sindresorhus/copy-file
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 9899ddbb450e422a5cddeb7ec498c43d95d2ea61
Choose a base ref
...
head repository: sindresorhus/copy-file
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 642f50d10c2f55360f61a497e8dd2184d201787c
Choose a head ref

Commits on Apr 29, 2019

  1. Add Node.js 12 to testing (#35)

    coreyfarrell authored and sindresorhus committed Apr 29, 2019
    Copy the full SHA
    82a5286 View commit details

Commits on May 28, 2019

  1. Create funding.yml

    sindresorhus authored May 28, 2019
    Copy the full SHA
    d3ed7b9 View commit details

Commits on Jun 11, 2019

  1. Meta tweaks

    sindresorhus committed Jun 11, 2019
    Copy the full SHA
    edcc055 View commit details
  2. Test on Windows too

    sindresorhus committed Jun 11, 2019
    Copy the full SHA
    4e1367f View commit details

Commits on Feb 24, 2020

  1. Copy the full SHA
    4668c5a View commit details
  2. Require Node.js 10

    sindresorhus committed Feb 24, 2020
    Copy the full SHA
    df050ac View commit details
  3. Copy the full SHA
    6d92d5c View commit details
  4. Copy the full SHA
    1b5e072 View commit details
  5. 8.0.0

    sindresorhus committed Feb 24, 2020
    Copy the full SHA
    11b236d View commit details
  6. Copy the full SHA
    d7fab4c View commit details
  7. 8.0.1

    sindresorhus committed Feb 24, 2020
    Copy the full SHA
    1cda1f2 View commit details

Commits on Mar 5, 2020

  1. Stop preserving ownership, stop performing chmod after copyFileSync (

    …#39)
    
    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    sindresorhus authored Mar 5, 2020
    Copy the full SHA
    4e536d5 View commit details
  2. Meta tweaks

    sindresorhus committed Mar 5, 2020
    Copy the full SHA
    5f5151d View commit details
  3. 9.0.0

    sindresorhus committed Mar 5, 2020
    Copy the full SHA
    8c7ac33 View commit details

Commits on Jan 1, 2021

  1. Copy the full SHA
    694ddb2 View commit details

Commits on Jun 13, 2021

  1. Add directoryMode option (#44)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    dittyroma and sindresorhus authored Jun 13, 2021
    Copy the full SHA
    4eb8f47 View commit details
  2. Meta tweaks

    sindresorhus committed Jun 13, 2021
    Copy the full SHA
    bedd8ce View commit details
  3. 9.1.0

    sindresorhus committed Jun 13, 2021
    Copy the full SHA
    589c637 View commit details

Commits on Apr 7, 2022

  1. Copy the full SHA
    778ecdb View commit details
  2. Copy the full SHA
    5dbf6dc View commit details

Commits on May 8, 2022

  1. Add cwd option (#46)

    jopemachine authored May 8, 2022
    Copy the full SHA
    3fe6ab4 View commit details

Commits on Jun 21, 2022

  1. Add onProgress option (#52)

    Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
    fitiskin and sindresorhus authored Jun 21, 2022
    Copy the full SHA
    cf322af View commit details
  2. Copy the full SHA
    176d12f View commit details
  3. 10.0.0

    sindresorhus committed Jun 21, 2022
    Copy the full SHA
    642f50d View commit details
Showing with 679 additions and 628 deletions.
  1. +26 −0 .github/workflows/main.yml
  2. +0 −9 .travis.yml
  3. +10 −0 copy-file-error.js
  4. +0 −12 cp-file-error.js
  5. +33 −55 fs.js
  6. +90 −60 index.d.ts
  7. +74 −62 index.js
  8. +40 −23 index.test-d.ts
  9. +1 −1 license
  10. +30 −27 package.json
  11. +0 −34 progress-emitter.js
  12. +48 −41 readme.md
  13. +126 −114 test/async.js
  14. +19 −0 test/helpers/_assert.js
  15. +30 −0 test/helpers/_fs-errors.js
  16. +0 −18 test/helpers/assert.js
  17. +0 −33 test/helpers/fs-errors.js
  18. +49 −35 test/progress.js
  19. +103 −104 test/sync.js
26 changes: 26 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
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:
- 18
- 16
- 14
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- run: npm install
- run: npm test
9 changes: 0 additions & 9 deletions .travis.yml

This file was deleted.

10 changes: 10 additions & 0 deletions copy-file-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import NestedError from 'nested-error-stacks';

// TODO: Use `Error#cause`.
export default class CopyFileError extends NestedError {
constructor(message, nested) {
super(message, nested);
Object.assign(this, nested);
this.name = 'CopyFileError';
}
}
12 changes: 0 additions & 12 deletions cp-file-error.js

This file was deleted.

88 changes: 33 additions & 55 deletions fs.js
Original file line number Diff line number Diff line change
@@ -1,99 +1,77 @@
'use strict';
const {promisify} = require('util');
const fs = require('graceful-fs');
const makeDir = require('make-dir');
const pEvent = require('p-event');
const CpFileError = require('./cp-file-error');
import {promisify} from 'node:util';
import fs from 'graceful-fs';
import {pEvent} from 'p-event';
import CopyFileError from './copy-file-error.js';

const stat = promisify(fs.stat);
const lstat = promisify(fs.lstat);
const utimes = promisify(fs.utimes);
const chmod = promisify(fs.chmod);
const chown = promisify(fs.chown);
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);

exports.closeSync = fs.closeSync.bind(fs);
exports.createWriteStream = fs.createWriteStream.bind(fs);
export const closeSync = fs.closeSync.bind(fs);
export const createWriteStream = fs.createWriteStream.bind(fs);

exports.createReadStream = async (path, options) => {
export async function createReadStream(path, options) {
const read = fs.createReadStream(path, options);

try {
await pEvent(read, ['readable', 'end']);
} catch (error) {
throw new CpFileError(`Cannot read from \`${path}\`: ${error.message}`, error);
throw new CopyFileError(`Cannot read from \`${path}\`: ${error.message}`, error);
}

return read;
};

exports.stat = path => stat(path).catch(error => {
throw new CpFileError(`Cannot stat path \`${path}\`: ${error.message}`, error);
});
}

exports.lstat = path => lstat(path).catch(error => {
throw new CpFileError(`lstat \`${path}\` failed: ${error.message}`, error);
export const stat = path => statP(path).catch(error => {
throw new CopyFileError(`Cannot stat path \`${path}\`: ${error.message}`, error);
});

exports.utimes = (path, atime, mtime) => utimes(path, atime, mtime).catch(error => {
throw new CpFileError(`utimes \`${path}\` failed: ${error.message}`, error);
export const lstat = path => lstatP(path).catch(error => {
throw new CopyFileError(`lstat \`${path}\` failed: ${error.message}`, error);
});

exports.chmod = (path, mode) => chmod(path, mode).catch(error => {
throw new CpFileError(`chmod \`${path}\` failed: ${error.message}`, error);
export const utimes = (path, atime, mtime) => utimesP(path, atime, mtime).catch(error => {
throw new CopyFileError(`utimes \`${path}\` failed: ${error.message}`, error);
});

exports.chown = (path, uid, gid) => chown(path, uid, gid).catch(error => {
throw new CpFileError(`chown \`${path}\` failed: ${error.message}`, error);
export const chmod = (path, mode) => chmodP(path, mode).catch(error => {
throw new CopyFileError(`chmod \`${path}\` failed: ${error.message}`, error);
});

exports.statSync = path => {
export const statSync = path => {
try {
return fs.statSync(path);
} catch (error) {
throw new CpFileError(`stat \`${path}\` failed: ${error.message}`, error);
throw new CopyFileError(`stat \`${path}\` failed: ${error.message}`, error);
}
};

exports.utimesSync = (path, atime, mtime) => {
export const utimesSync = (path, atime, mtime) => {
try {
return fs.utimesSync(path, atime, mtime);
} catch (error) {
throw new CpFileError(`utimes \`${path}\` failed: ${error.message}`, error);
}
};

exports.chmodSync = (path, mode) => {
try {
return fs.chmodSync(path, mode);
} catch (error) {
throw new CpFileError(`chmod \`${path}\` failed: ${error.message}`, error);
}
};

exports.chownSync = (path, uid, gid) => {
try {
return fs.chownSync(path, uid, gid);
} catch (error) {
throw new CpFileError(`chown \`${path}\` failed: ${error.message}`, error);
throw new CopyFileError(`utimes \`${path}\` failed: ${error.message}`, error);
}
};

exports.makeDir = path => makeDir(path, {fs}).catch(error => {
throw new CpFileError(`Cannot create directory \`${path}\`: ${error.message}`, error);
export const makeDirectory = (path, options) => makeDirectoryP(path, {...options, recursive: true}).catch(error => {
throw new CopyFileError(`Cannot create directory \`${path}\`: ${error.message}`, error);
});

exports.makeDirSync = path => {
export const makeDirectorySync = (path, options) => {
try {
makeDir.sync(path, {fs});
fs.mkdirSync(path, {...options, recursive: true});
} catch (error) {
throw new CpFileError(`Cannot create directory \`${path}\`: ${error.message}`, error);
throw new CopyFileError(`Cannot create directory \`${path}\`: ${error.message}`, error);
}
};

exports.copyFileSync = (source, destination, flags) => {
export const copyFileSync = (source, destination, flags) => {
try {
fs.copyFileSync(source, destination, flags);
} catch (error) {
throw new CpFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error);
throw new CopyFileError(`Cannot copy from \`${source}\` to \`${destination}\`: ${error.message}`, error);
}
};
150 changes: 90 additions & 60 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,105 @@
declare namespace cpFile {
interface Options {
/**
Overwrite existing file.
@default true
*/
readonly overwrite?: boolean;
}

interface ProgressData {
/**
Absolute path to source.
*/
src: string;

/**
Absolute path to destination.
*/
dest: string;

/**
File size in bytes.
*/
size: number;

/**
Copied size in bytes.
*/
written: number;

/**
Copied percentage, a value between `0` and `1`.
*/
percent: number;
}

interface ProgressEmitter {
/**
For empty files, the `progress` event is emitted only once.
*/
on(event: 'progress', handler: (data: ProgressData) => void): Promise<void>;
}
export interface 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;
}

declare const cpFile: {
export interface AsyncOptions {
/**
Copy a file.
The given function is called whenever there is measurable progress.
@param source - File you want to copy.
@param destination - Where you want the file copied.
@returns A `Promise` that resolves when the file is copied.
Note: For empty files, the `onProgress` event is emitted only once.
@example
```
import cpFile = require('cp-file');
import {copyFile} from 'cp-file';
(async () => {
await cpFile('source/unicorn.png', 'destination/unicorn.png');
console.log('File copied');
})();
await copyFile('source/unicorn.png', 'destination/unicorn.png', {
onProgress: progress => {
// …
}
});
```
*/
(source: string, destination: string, options?: cpFile.Options): Promise<void> & cpFile.ProgressEmitter;
readonly onProgress?: (progress: ProgressData) => void;
}

export interface ProgressData {
/**
Copy a file synchronously.
Absolute path to source.
*/
sourcePath: string;

@param source - File you want to copy.
@param destination - Where you want the file copied.
/**
Absolute path to destination.
*/
sync(source: string, destination: string, options?: cpFile.Options): void;
};
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.
@example
```
import {copyFile} from 'cp-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.
@example
```
import {copyFileSync} from 'cp-file';
export = cpFile;
copyFileSync('source/unicorn.png', 'destination/unicorn.png');
```
*/
export function copyFileSync(source: string, destination: string, options?: Options): void;
136 changes: 74 additions & 62 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,109 +1,122 @@
'use strict';
const path = require('path');
const {constants: fsConstants} = require('fs');
const pEvent = require('p-event');
const CpFileError = require('./cp-file-error');
const fs = require('./fs');
const ProgressEmitter = require('./progress-emitter');

const cpFileAsync = async (source, destination, options, progressEmitter) => {
import path from 'node:path';
import {constants as fsConstants} from 'node:fs';
import {pEvent} from 'p-event';
import CopyFileError from './copy-file-error.js';
import * as fs from './fs.js';

const copyFileAsync = async (source, destination, options) => {
let readError;
const stat = await fs.stat(source);
progressEmitter.size = stat.size;

const read = await fs.createReadStream(source);
await fs.makeDir(path.dirname(destination));
const write = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'});
read.on('data', () => {
progressEmitter.written = write.bytesWritten;
const {size} = await fs.stat(source);

const readStream = await fs.createReadStream(source);
await fs.makeDirectory(path.dirname(destination), {mode: options.directoryMode});
const writeStream = fs.createWriteStream(destination, {flags: options.overwrite ? 'w' : 'wx'});

const emitProgress = writtenBytes => {
if (typeof options.onProgress !== 'function') {
return;
}

options.onProgress({
sourcePath: path.resolve(source),
destinationPath: path.resolve(destination),
size,
writtenBytes,
percent: writtenBytes === size ? 1 : writtenBytes / size,
});
};

readStream.on('data', () => {
emitProgress(writeStream.bytesWritten);
});
read.once('error', error => {
readError = new CpFileError(`Cannot read from \`${source}\`: ${error.message}`, error);
write.end();

readStream.once('error', error => {
readError = new CopyFileError(`Cannot read from \`${source}\`: ${error.message}`, error);
});

let updateStats = false;
let shouldUpdateStats = false;
try {
const writePromise = pEvent(write, 'close');
read.pipe(write);
const writePromise = pEvent(writeStream, 'close');
readStream.pipe(writeStream);
await writePromise;
progressEmitter.written = progressEmitter.size;
updateStats = true;
emitProgress(size);
shouldUpdateStats = true;
} catch (error) {
if (options.overwrite || error.code !== 'EEXIST') {
throw new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error);
}
throw new CopyFileError(`Cannot write to \`${destination}\`: ${error.message}`, error);
}

if (readError) {
throw readError;
}

if (updateStats) {
if (shouldUpdateStats) {
const stats = await fs.lstat(source);

return Promise.all([
fs.utimes(destination, stats.atime, stats.mtime),
fs.chmod(destination, stats.mode),
fs.chown(destination, stats.uid, stats.gid)
]);
}
};

const cpFile = (source, destination, options) => {
if (!source || !destination) {
return Promise.reject(new CpFileError('`source` and `destination` required'));
const resolvePath = (cwd, sourcePath, destinationPath) => {
sourcePath = path.resolve(cwd, sourcePath);
destinationPath = path.resolve(cwd, destinationPath);

return {
sourcePath,
destinationPath,
};
};

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 progressEmitter = new ProgressEmitter(path.resolve(source), path.resolve(destination));
const promise = cpFileAsync(source, destination, options, progressEmitter);
promise.on = (...args) => {
progressEmitter.on(...args);
return promise;
...options,
};

return promise;
};

module.exports = cpFile;
return copyFileAsync(sourcePath, destinationPath, options);
}

const checkSourceIsFile = (stat, source) => {
if (stat.isDirectory()) {
throw Object.assign(new CpFileError(`EISDIR: illegal operation on a directory '${source}'`), {
throw Object.assign(new CopyFileError(`EISDIR: illegal operation on a directory '${source}'`), {
errno: -21,
code: 'EISDIR',
source
source,
});
}
};

const fixupAttributes = (destination, stat) => {
fs.chmodSync(destination, stat.mode);
fs.chownSync(destination, stat.uid, stat.gid);
};
export function copyFileSync(sourcePath, destinationPath, options = {}) {
if (!sourcePath || !destinationPath) {
throw new CopyFileError('`source` and `destination` required');
}

module.exports.sync = (source, destination, options) => {
if (!source || !destination) {
throw new CpFileError('`source` and `destination` required');
if (options.cwd) {
({sourcePath, destinationPath} = resolvePath(options.cwd, sourcePath, destinationPath));
}

options = {
overwrite: true,
...options
...options,
};

const stat = fs.statSync(source);
checkSourceIsFile(stat, source);
fs.makeDirSync(path.dirname(destination));
const stat = fs.statSync(sourcePath);
checkSourceIsFile(stat, sourcePath);
fs.makeDirectorySync(path.dirname(destinationPath), {mode: options.directoryMode});

const flags = options.overwrite ? null : fsConstants.COPYFILE_EXCL;
try {
fs.copyFileSync(source, destination, flags);
fs.copyFileSync(sourcePath, destinationPath, flags);
} catch (error) {
if (!options.overwrite && error.code === 'EEXIST') {
return;
@@ -112,6 +125,5 @@ module.exports.sync = (source, destination, options) => {
throw error;
}

fs.utimesSync(destination, stat.atime, stat.mtime);
fixupAttributes(destination, stat);
};
fs.utimesSync(destinationPath, stat.atime, stat.mtime);
}
63 changes: 40 additions & 23 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,48 @@
import {expectType} from 'tsd';
import cpFile = require('.');
import {ProgressEmitter, ProgressData} from '.';
import {expectError, expectType} from 'tsd';
import {copyFile, copyFileSync, ProgressData} from './index.js';

expectType<Promise<void> & ProgressEmitter>(
cpFile('source/unicorn.png', 'destination/unicorn.png')
expectType<Promise<void> >(
copyFile('source/unicorn.png', 'destination/unicorn.png'),
);
expectType<Promise<void> & ProgressEmitter>(
cpFile('source/unicorn.png', 'destination/unicorn.png', {overwrite: false})
expectType<Promise<void>>(
copyFile('source/unicorn.png', 'destination/unicorn.png', {overwrite: false}),
);
expectType<Promise<void>>(
cpFile('source/unicorn.png', 'destination/unicorn.png').on(
'progress',
data => {
expectType<ProgressData>(data);

expectType<string>(data.src);
expectType<string>(data.dest);
expectType<number>(data.size);
expectType<number>(data.written);
expectType<number>(data.percent);
}
)
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>(cpFile.sync('source/unicorn.png', 'destination/unicorn.png'));
expectType<void>(copyFileSync('source/unicorn.png', 'destination/unicorn.png'));
expectType<void>(
copyFileSync('source/unicorn.png', 'destination/unicorn.png', {
overwrite: false,
}),
);
expectType<void>(
cpFile.sync('source/unicorn.png', 'destination/unicorn.png', {
overwrite: false
})
copyFileSync('source/unicorn.png', 'destination/unicorn.png', {
directoryMode: 0o700,
}),
);
expectError(
copyFileSync('source/unicorn.png', 'destination/unicorn.png', {
directoryMode: '700',
}),
);
2 changes: 1 addition & 1 deletion license
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com)
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:

57 changes: 30 additions & 27 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,33 +1,29 @@
{
"name": "cp-file",
"version": "7.0.0",
"version": "10.0.0",
"description": "Copy a file",
"license": "MIT",
"repository": "sindresorhus/cp-file",
"funding": "https://github.com/sponsors/sindresorhus",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "sindresorhus.com"
"url": "https://sindresorhus.com"
},
"maintainers": [
{
"name": "Michael Mayer",
"email": "michael@schnittstabil.de",
"url": "schnittstabil.de"
}
],
"type": "module",
"exports": "./index.js",
"types": "./index.d.ts",
"engines": {
"node": ">=8"
"node": ">=14.16"
},
"scripts": {
"test": "xo && nyc ava && tsd"
},
"files": [
"cp-file-error.js",
"fs.js",
"index.js",
"index.d.ts",
"progress-emitter.js"
"copy-file-error.js",
"fs.js"
],
"keywords": [
"copy",
@@ -45,21 +41,28 @@
"contents"
],
"dependencies": {
"graceful-fs": "^4.1.2",
"make-dir": "^3.0.0",
"nested-error-stacks": "^2.0.0",
"p-event": "^4.1.0"
"graceful-fs": "^4.2.10",
"nested-error-stacks": "^2.1.1",
"p-event": "^5.0.1"
},
"devDependencies": {
"ava": "^1.4.1",
"clear-module": "^3.1.0",
"coveralls": "^3.0.3",
"del": "^4.1.0",
"import-fresh": "^3.0.0",
"nyc": "^13.3.0",
"sinon": "^7.3.1",
"tsd": "^0.7.2",
"uuid": "^3.3.2",
"xo": "^0.24.0"
"ava": "^4.3.0",
"clear-module": "^4.1.2",
"coveralls": "^3.1.1",
"del": "^6.1.1",
"import-fresh": "^3.3.0",
"nyc": "^15.1.0",
"sinon": "^14.0.0",
"tsd": "^0.21.0",
"xo": "^0.50.0"
},
"xo": {
"rules": {
"unicorn/string-content": "off",
"ava/assertion-arguments": "off"
}
},
"ava": {
"workerThreads": false
}
}
34 changes: 0 additions & 34 deletions progress-emitter.js

This file was deleted.

89 changes: 48 additions & 41 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,49 +1,44 @@
# cp-file [![Build Status](https://travis-ci.org/sindresorhus/cp-file.svg?branch=master)](https://travis-ci.org/sindresorhus/cp-file) [![Coverage Status](https://coveralls.io/repos/github/sindresorhus/cp-file/badge.svg?branch=master)](https://coveralls.io/github/sindresorhus/cp-file?branch=master)
# cp-file

> Copy a file

## Highlights

- Fast by using streams in the async version and [`fs.copyFileSync()`](https://nodejs.org/api/fs.html#fs_fs_copyfilesync_src_dest_flags) in the synchronous version.
- 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](#optionsoverwrite).
- Preserves file mode, [but not ownership](https://github.com/sindresorhus/cp-file/issues/22#issuecomment-502079547).
- User-friendly errors.


## Install

```
$ npm install cp-file
```sh
npm install cp-file
```


## Usage

```js
const cpFile = require('cp-file');
import {copyFile} from 'cp-file';

(async () => {
await cpFile('source/unicorn.png', 'destination/unicorn.png');
console.log('File copied');
})();
await copyFile('source/unicorn.png', 'destination/unicorn.png');
console.log('File copied');
```


## API

### cpFile(source, destination, [options])
### copyFile(source, destination, options?)

Returns a `Promise` that resolves when the file is copied.

### cpFile.sync(source, destination, [options])
### copyFileSync(source, destination, options?)

#### source

Type: `string`

File you want to copy.
The file you want to copy.

#### destination

@@ -53,62 +48,74 @@ Where you want the file copied.

#### options

Type: `Object`
Type: `object`

##### overwrite

Type: `boolean`<br>
Type: `boolean`\
Default: `true`

Overwrite existing file.
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.

### cpFile.on('progress', handler)
##### directoryMode

Progress reporting. Only available when using the async method.
Type: `number`\
Default: `0o777`

#### handler(data)
[Permissions](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) for created directories.

Type: `Function`
It has no effect on Windows.

##### data
##### onProgress

Type: `(progress: ProgressData) => void`

The given function is called whenever there is measurable progress.

Only available when using the async method.

###### `ProgressData`

```js
{
src: string,
dest: string,
sourcePath: string,
destinationPath: string,
size: number,
written: number,
writtenBytes: number,
percent: number
}
```

- `src` and `dest` are absolute paths.
- `size` and `written` are in bytes.
- `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 `progress` event is emitted only once.
- The `.on()` method is available only right after the initial `cpFile()` call. So make sure
you add a `handler` before `.then()`:
- For empty files, the `onProgress` callback function is emitted only once.

```js
(async () => {
await cpFile(source, destination).on('progress', data => {
import {copyFile} from 'cp-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


## License

MIT © [Sindre Sorhus](https://sindresorhus.com)
240 changes: 126 additions & 114 deletions test/async.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import crypto from 'crypto';
import path from 'path';
import fs from 'graceful-fs';
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 del from 'del';
import test from 'ava';
import uuid from 'uuid';
import sinon from 'sinon';
import cpFile from '..';
import assertDateEqual from './helpers/assert';
import {buildEACCES, buildEIO, buildENOSPC, buildENOENT, buildEPERM} from './helpers/fs-errors';
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;

@@ -18,144 +21,160 @@ test.before(() => {
});

test.beforeEach(t => {
t.context.source = uuid.v4();
t.context.destination = uuid.v4();
t.context.source = crypto.randomUUID();
t.context.destination = crypto.randomUUID();
t.context.creates = [t.context.source, t.context.destination];
});

test.afterEach.always(t => {
t.context.creates.forEach(path => del.sync(path));
del.sync(t.context.creates);
});

test('reject an Error on missing `source`', async t => {
await t.throwsAsync(cpFile(), /`source`/);
await t.throwsAsync(copyFile(), {
message: /`source`/,
});
});

test('reject an Error on missing `destination`', async t => {
await t.throwsAsync(cpFile('TARGET'), /`destination`/);
await t.throwsAsync(copyFile('TARGET'), {
message: /`destination`/,
});
});

test('copy a file', async t => {
await cpFile('license', t.context.destination);
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 cpFile(t.context.source, t.context.destination);
await copyFile(t.context.source, t.context.destination);
t.is(fs.readFileSync(t.context.destination, 'utf8'), '');
});

test('copy big files', async t => {
const buf = crypto.randomBytes(THREE_HUNDRED_KILO);
fs.writeFileSync(t.context.source, buf);
await cpFile(t.context.source, t.context.destination);
t.true(buf.equals(fs.readFileSync(t.context.destination)));
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 cpFile('license', t.context.destination, 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 cpFile('license', t.context.destination, {overwrite: true});
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 cpFile('license', 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, '');
await cpFile('license', t.context.destination, {overwrite: false});
t.is(fs.readFileSync(t.context.destination, 'utf8'), '');
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(() => cpFile('node_modules', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.code, 'EISDIR', error);
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);
}, /ENOENT/);
}, {
message: /ENOENT/,
});
});

test('do not create `destination` directory on unreadable `source`', async t => {
const error = await t.throwsAsync(() => cpFile('node_modules', 'subdir/' + uuid.v4()));
t.is(error.name, 'CpFileError', error);
t.is(error.code, 'EISDIR', error);
const error = await t.throwsAsync(copyFile('node_modules', path.join('subdir', crypto.randomUUID())));

t.is(error.name, 'CopyFileError', error.message);
t.is(error.code, 'EISDIR', error.message);

t.throws(() => {
fs.statSync('subdir');
}, /ENOENT/);
}, {
message: /ENOENT/,
});
});

test('preserve timestamps', async t => {
await cpFile('license', t.context.destination);
await copyFile('license', t.context.destination);
const licenseStats = fs.lstatSync('license');
const tmpStats = fs.lstatSync(t.context.destination);
assertDateEqual(t, licenseStats.atime, tmpStats.atime);
assertDateEqual(t, licenseStats.mtime, tmpStats.mtime);
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 cpFile('license', t.context.destination);
const licenseStats = fs.lstatSync('license');
const tmpStats = fs.lstatSync(t.context.destination);
t.is(licenseStats.mode, tmpStats.mode);
});

test('preserve ownership', async t => {
await cpFile('license', t.context.destination);
await copyFile('license', t.context.destination);
const licenseStats = fs.lstatSync('license');
const tmpStats = fs.lstatSync(t.context.destination);
t.is(licenseStats.gid, tmpStats.gid);
t.is(licenseStats.uid, tmpStats.uid);
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(cpFile('NO_ENTRY', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.code, 'ENOENT', error);
t.regex(error.message, /`NO_ENTRY`/, error);
t.regex(error.stack, /`NO_ENTRY`/, error);
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('rethrow mkdir EACCES errors', async t => {
const dirPath = '/root/NO_ACCESS_' + uuid.v4();
const dest = dirPath + '/' + uuid.v4();
const mkdirError = buildEACCES(dirPath);
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(cpFile('license', dest));
t.is(error.name, 'CpFileError', error);
t.is(error.errno, mkdirError.errno, error);
t.is(error.code, mkdirError.code, error);
t.is(error.path, mkdirError.path, error);
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('rethrow ENOSPC errors', async t => {
test.serial.failing('rethrow ENOSPC errors', async t => {
const {createWriteStream} = fs;
const noSpaceError = buildENOSPC();
let called = false;
let isCalled = false;

fs.createWriteStream = (path, options) => {
const stream = createWriteStream(path, options);
if (path === t.context.destination) {
stream.on('pipe', () => {
if (!called) {
called = true;
if (!isCalled) {
isCalled = true;
stream.emit('error', noSpaceError);
}
});
@@ -164,89 +183,73 @@ test.serial('rethrow ENOSPC errors', async t => {
return stream;
};

clearModule('../fs');
const uncached = importFresh('..');
clearModule('../fs.js');
const uncached = importFresh('../index.js');
const error = await t.throwsAsync(uncached('license', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.errno, noSpaceError.errno, error);
t.is(error.code, noSpaceError.code, error);
t.true(called);
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('rethrow stat errors', async t => {
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');
const uncached = importFresh('..');
clearModule('../fs.js');
const uncached = importFresh('../index.js');
const error = await t.throwsAsync(uncached(t.context.source, t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.errno, fstatError.errno, error);
t.is(error.code, fstatError.code, error);
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('rethrow utimes errors', async t => {
test.serial.failing('rethrow utimes errors', async t => {
const utimesError = buildENOENT();

fs.utimes = sinon.stub(fs, 'utimes').throws(utimesError);

clearModule('../fs');
const uncached = importFresh('..');
clearModule('../fs.js');
const uncached = importFresh('../index.js');
const error = await t.throwsAsync(uncached('license', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.code, 'ENOENT', error);
t.is(error.name, 'CopyFileError', error.message);
t.is(error.code, 'ENOENT', error.message);
t.true(fs.utimes.called);

fs.utimes.restore();
});

test.serial('rethrow chmod errors', async t => {
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');
const uncached = importFresh('..');
clearModule('../fs.js');
const uncached = importFresh('../index.js');
const error = await t.throwsAsync(uncached('license', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.code, chmodError.code, error);
t.is(error.path, chmodError.path, error);
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('rethrow chown errors', async t => {
const chownError = buildEPERM(t.context.destination, 'chown');

fs.chown = sinon.stub(fs, 'chown').throws(chownError);

clearModule('../fs');
const uncached = importFresh('..');
const error = await t.throwsAsync(uncached('license', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.code, chownError.code, error);
t.is(error.path, chownError.path, error);
t.true(fs.chown.called);

fs.chown.restore();
});

test.serial('rethrow read after open errors', async t => {
test.serial.failing('rethrow read after open errors', async t => {
const {createWriteStream, createReadStream} = fs;
let calledWriteEnd = 0;
let readStream;
const readError = buildEIO();
const readError = buildERRSTREAMWRITEAFTEREND();

fs.createWriteStream = (...args) => {
const stream = createWriteStream(...args);
fs.createWriteStream = (...arguments_) => {
const stream = createWriteStream(...arguments_);
const {end} = stream;

stream.on('pipe', () => {
@@ -261,21 +264,30 @@ test.serial('rethrow read after open errors', async t => {
return stream;
};

fs.createReadStream = (...args) => {
fs.createReadStream = (...arguments_) => {
/* Fake stream */
readStream = createReadStream(...args);
readStream = createReadStream(...arguments_);
readStream.pause();

return readStream;
};

clearModule('../fs');
const uncached = importFresh('..');
clearModule('../fs.js');
const uncached = importFresh('../index.js');
const error = await t.throwsAsync(uncached('license', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.errno, readError.errno, error);
t.is(error.code, readError.code, error);
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'}));
});
19 changes: 19 additions & 0 deletions test/helpers/_assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
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);
}
30 changes: 30 additions & 0 deletions test/helpers/_fs-errors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
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',
});
18 changes: 0 additions & 18 deletions test/helpers/assert.js

This file was deleted.

33 changes: 0 additions & 33 deletions test/helpers/fs-errors.js

This file was deleted.

84 changes: 49 additions & 35 deletions test/progress.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import crypto from 'crypto';
import path from 'path';
import fs from 'graceful-fs';
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 del from 'del';
import test from 'ava';
import uuid from 'uuid';
import cpFile from '..';
import {copyFile} from '../index.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const THREE_HUNDRED_KILO = (100 * 3 * 1024) + 1;

@@ -13,56 +16,67 @@ test.before(() => {
});

test.beforeEach(t => {
t.context.source = uuid.v4();
t.context.destination = uuid.v4();
t.context.source = crypto.randomUUID();
t.context.destination = crypto.randomUUID();
t.context.creates = [t.context.source, t.context.destination];
});

test.afterEach.always(t => {
t.context.creates.forEach(path => del.sync(path));
for (const path of t.context.creates) {
del.sync(path);
}
});

test('report progress', async t => {
const buf = crypto.randomBytes(THREE_HUNDRED_KILO);
fs.writeFileSync(t.context.source, buf);

let calls = 0;
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
calls++;
t.is(typeof progress.src, 'string');
t.is(typeof progress.dest, 'string');
t.is(typeof progress.size, 'number');
t.is(typeof progress.written, 'number');
t.is(typeof progress.percent, 'number');
t.is(progress.size, THREE_HUNDRED_KILO);
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(calls > 0);
t.true(callCount > 0);
});

test('report progress of 100% on end', async t => {
const buf = crypto.randomBytes(THREE_HUNDRED_KILO);
fs.writeFileSync(t.context.source, buf);
const buffer = crypto.randomBytes(THREE_HUNDRED_KILO);
fs.writeFileSync(t.context.source, buffer);

let lastEvent;
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
lastEvent = progress;
let lastRecord;

await copyFile(t.context.source, t.context.destination, {
onProgress(progress) {
lastRecord = progress;
},
});

t.is(lastEvent.percent, 1);
t.is(lastEvent.written, THREE_HUNDRED_KILO);
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 calls = 0;
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
calls++;
t.is(progress.size, 0);
t.is(progress.written, 0);
t.is(progress.percent, 1);
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(calls, 1);
t.is(callCount, 1);
});
207 changes: 103 additions & 104 deletions test/sync.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import crypto from 'crypto';
import path from 'path';
import fs from 'graceful-fs';
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 del from 'del';
import test from 'ava';
import uuid from 'uuid';
import sinon from 'sinon';
import cpFile from '..';
import assertDateEqual from './helpers/assert';
import {buildEACCES, buildENOSPC, buildEBADF, buildEPERM} from './helpers/fs-errors';
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;

@@ -16,230 +19,226 @@ test.before(() => {
});

test.beforeEach(t => {
t.context.source = uuid.v4();
t.context.destination = uuid.v4();
t.context.source = crypto.randomUUID();
t.context.destination = crypto.randomUUID();
t.context.creates = [t.context.source, t.context.destination];
});

test.afterEach.always(t => {
t.context.creates.forEach(path => del.sync(path));
for (const path_ of t.context.creates) {
del.sync(path_);
}
});

test('throw an Error on missing `source`', t => {
t.throws(() => {
cpFile.sync();
}, /`source`/);
copyFileSync();
}, {
message: /`source`/,
});
});

test('throw an Error on missing `destination`', t => {
t.throws(() => {
cpFile.sync('TARGET');
}, /`destination`/);
copyFileSync('TARGET');
}, {
message: /`destination`/,
});
});

test('copy a file', t => {
cpFile.sync('license', t.context.destination);
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, '');
cpFile.sync(t.context.source, t.context.destination);
copyFileSync(t.context.source, t.context.destination);
t.is(fs.readFileSync(t.context.destination, 'utf8'), '');
});

test('copy big files', t => {
const buf = crypto.randomBytes(THREE_HUNDRED_KILO);
fs.writeFileSync(t.context.source, buf);
cpFile.sync(t.context.source, t.context.destination);
t.true(buf.equals(fs.readFileSync(t.context.destination)));
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 = {};
cpFile.sync('license', t.context.destination, options);
copyFileSync('license', t.context.destination, options);
t.false('overwrite' in options);
});

test('overwrite when enabled', t => {
fs.writeFileSync(t.context.destination, '');
cpFile.sync('license', t.context.destination, {overwrite: true});
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, '');
cpFile.sync('license', 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, '');
cpFile.sync('license', t.context.destination, {overwrite: false});
copyFileSync('license', t.context.destination, {overwrite: false});
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(
() => {
cpFile.sync('node_modules', t.context.destination);
copyFileSync('node_modules', t.context.destination);
},
{
name: 'CpFileError',
code: 'EISDIR'
}
name: 'CopyFileError',
code: 'EISDIR',
},
);

t.throws(() => {
fs.statSync(t.context.destination);
}, /ENOENT/);
}, {
message: /ENOENT/,
});
});

test('do not create `destination` directory on unreadable `source`', t => {
t.throws(
() => {
cpFile.sync('node_modules', 'subdir/' + uuid.v4());
copyFileSync('node_modules', `subdir/${crypto.randomUUID()}`);
},
{
name: 'CpFileError',
code: 'EISDIR'
}
name: 'CopyFileError',
code: 'EISDIR',
},
);

t.throws(() => {
fs.statSync('subdir');
}, /ENOENT/);
}, {
message: /ENOENT/,
});
});

test('preserve timestamps', t => {
cpFile.sync('license', t.context.destination);
copyFileSync('license', t.context.destination);
const licenseStats = fs.lstatSync('license');
const tmpStats = fs.lstatSync(t.context.destination);
assertDateEqual(t, licenseStats.atime, tmpStats.atime);
assertDateEqual(t, licenseStats.mtime, tmpStats.mtime);
const temporaryStats = fs.lstatSync(t.context.destination);
assertDateEqual(t, licenseStats.atime, temporaryStats.atime);
assertDateEqual(t, licenseStats.mtime, temporaryStats.mtime);
});

test('preserve mode', t => {
cpFile.sync('license', t.context.destination);
copyFileSync('license', t.context.destination);
const licenseStats = fs.lstatSync('license');
const tmpStats = fs.lstatSync(t.context.destination);
t.is(licenseStats.mode, tmpStats.mode);
});

test('preserve ownership', t => {
cpFile.sync('license', t.context.destination);
const licenseStats = fs.lstatSync('license');
const tmpStats = fs.lstatSync(t.context.destination);
t.is(licenseStats.gid, tmpStats.gid);
t.is(licenseStats.uid, tmpStats.uid);
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(() => cpFile.sync('NO_ENTRY', t.context.destination));
t.is(error.name, 'CpFileError', error);
t.is(error.code, 'ENOENT', error);
t.regex(error.message, /`NO_ENTRY`/, error);
t.regex(error.stack, /`NO_ENTRY`/, error);
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('rethrow mkdir EACCES errors', t => {
const dirPath = '/root/NO_ACCESS_' + uuid.v4();
const dest = dirPath + '/' + uuid.v4();
const mkdirError = buildEACCES(dirPath);
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(() => {
cpFile.sync('license', dest);
copyFileSync('license', destination);
});
t.is(error.name, 'CpFileError', error);
t.is(error.errno, mkdirError.errno, error);
t.is(error.code, mkdirError.code, error);
t.is(error.path, mkdirError.path, error);
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 => {
test.failing('rethrow ENOSPC errors', t => {
const noSpaceError = buildENOSPC();

fs.writeFileSync(t.context.source, '');
fs.copyFileSync = sinon.stub(fs, 'copyFileSync').throws(noSpaceError);

const error = t.throws(() => {
cpFile.sync('license', t.context.destination);
copyFileSync('license', t.context.destination);
});
t.is(error.name, 'CpFileError', error);
t.is(error.errno, noSpaceError.errno, error);
t.is(error.code, noSpaceError.code, error);
t.true(fs.copyFileSync.called, 1);
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('rethrow stat errors', t => {
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(() => {
cpFile.sync(t.context.source, t.context.destination);
copyFileSync(t.context.source, t.context.destination);
});
t.is(error.name, 'CpFileError', error);
t.is(error.errno, statError.errno, error);
t.is(error.code, statError.code, error);
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('rethrow utimes errors', t => {
test.failing('rethrow utimes errors', t => {
const futimesError = buildEBADF();

fs.utimesSync = sinon.stub(fs, 'utimesSync').throws(futimesError);

const error = t.throws(() => {
cpFile.sync('license', t.context.destination);
copyFileSync('license', t.context.destination);
});
t.is(error.name, 'CpFileError', error);
t.is(error.errno, futimesError.errno, error);
t.is(error.code, futimesError.code, error);
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('rethrow chmod errors', t => {
const chmodError = buildEPERM(t.context.destination, 'chmod');

fs.chmodSync = sinon.stub(fs, 'chmodSync').throws(chmodError);

test('cwd option', t => {
const error = t.throws(() => {
cpFile.sync('license', t.context.destination);
copyFileSync('sync.js', t.context.destination);
});
t.is(error.name, 'CpFileError', error);
t.is(error.errno, chmodError.errno, error);
t.is(error.code, chmodError.code, error);
t.true(fs.chmodSync.called);

fs.chmodSync.restore();
});

test('rethrow chown errors', t => {
const chownError = buildEPERM(t.context.destination, 'chown');

fs.chownSync = sinon.stub(fs, 'chownSync').throws(chownError);
t.is(error.name, 'CopyFileError');
t.is(error.code, 'ENOENT');

const error = t.throws(() => {
cpFile.sync('license', t.context.destination);
t.notThrows(() => {
copyFileSync('sync.js', t.context.destination, {cwd: 'test'});
});
t.is(error.name, 'CpFileError', error);
t.is(error.errno, chownError.errno, error);
t.is(error.code, chownError.code, error);
t.true(fs.chownSync.called);

fs.chownSync.restore();
});