Skip to content

Commit

Permalink
Add onProgress option (#52)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
fitiskin and sindresorhus committed Jun 21, 2022
1 parent 3fe6ab4 commit cf322af
Show file tree
Hide file tree
Showing 7 changed files with 158 additions and 60 deletions.
28 changes: 26 additions & 2 deletions index.d.ts
Expand Up @@ -26,6 +26,28 @@ declare namespace cpFile {
readonly cwd?: string;
}

interface AsyncOptions {
/**
The given function is called whenever there is measurable progress.
Note: For empty files, the `onProgress` event is emitted only once.
@example
```
import cpFile = require('cp-file');
(async () => {
await cpFile('source/unicorn.png', 'destination/unicorn.png', {
onProgress: progress => {
// ...
}
});
})();
```
*/
readonly onProgress?: (progress: ProgressData) => void;
}

interface ProgressData {
/**
Absolute path to source.
Expand Down Expand Up @@ -55,9 +77,11 @@ declare namespace cpFile {

interface ProgressEmitter {
/**
@deprecated Use `onProgress` option instead.
Note: For empty files, the `progress` event is emitted only once.
*/
on(event: 'progress', handler: (data: ProgressData) => void): Promise<void>;
on(event: 'progress', handler: AsyncOptions['onProgress']): Promise<void>;
}
}

Expand All @@ -79,7 +103,7 @@ declare const cpFile: {
})();
```
*/
(source: string, destination: string, options?: cpFile.Options): Promise<void> & cpFile.ProgressEmitter;
(source: string, destination: string, options?: cpFile.Options & cpFile.AsyncOptions): Promise<void> & cpFile.ProgressEmitter;

/**
Copy a file synchronously.
Expand Down
31 changes: 21 additions & 10 deletions index.js
Expand Up @@ -4,19 +4,31 @@ 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) => {
const cpFileAsync = async (source, destination, options) => {
let readError;
const stat = await fs.stat(source);
progressEmitter.size = stat.size;
const {size} = await fs.stat(source);

const readStream = await fs.createReadStream(source);
await fs.makeDir(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', () => {
progressEmitter.writtenBytes = writeStream.bytesWritten;
emitProgress(writeStream.bytesWritten);
});

readStream.once('error', error => {
Expand All @@ -32,7 +44,7 @@ const cpFileAsync = async (source, destination, options, progressEmitter) => {
const writePromise = pEvent(writeStream, 'close');
readStream.pipe(writeStream);
await writePromise;
progressEmitter.writtenBytes = progressEmitter.size;
emitProgress(size);
shouldUpdateStats = true;
} catch (error) {
throw new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error);
Expand Down Expand Up @@ -76,11 +88,10 @@ const cpFile = (sourcePath, destinationPath, options = {}) => {
...options
};

const progressEmitter = new ProgressEmitter(path.resolve(sourcePath), path.resolve(destinationPath));
const promise = cpFileAsync(sourcePath, destinationPath, options, progressEmitter);
const promise = cpFileAsync(sourcePath, destinationPath, options);

promise.on = (...arguments_) => {
progressEmitter.on(...arguments_);
promise.on = (_eventName, callback) => {
options.onProgress = callback;
return promise;
};

Expand Down
13 changes: 13 additions & 0 deletions index.test-d.ts
Expand Up @@ -18,6 +18,19 @@ expectError(
directoryMode: '700'
})
);
expectType<Promise<void> & ProgressEmitter>(
cpFile('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<Promise<void>>(
cpFile('source/unicorn.png', 'destination/unicorn.png').on(
'progress',
Expand Down
3 changes: 1 addition & 2 deletions package.json
Expand Up @@ -20,8 +20,7 @@
"cp-file-error.js",
"fs.js",
"index.js",
"index.d.ts",
"progress-emitter.js"
"index.d.ts"
],
"keywords": [
"copy",
Expand Down
35 changes: 0 additions & 35 deletions progress-emitter.js

This file was deleted.

44 changes: 43 additions & 1 deletion readme.md
Expand Up @@ -77,8 +77,50 @@ Default: `0o777`

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
const cpFile = require('cp-file');

(async () => {
await cpFile(source, destination, {
onProgress: progress => {
//
}
});
})();
```

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

> Deprecated. Use `onProgress` option instead.
Progress reporting. Only available when using the async method.

#### handler(data)
Expand All @@ -97,7 +139,7 @@ Type: `Function`
}
```

- `source` and `destination` are absolute paths.
- `sourcePath` and `destinationPath` are absolute paths.
- `size` and `writtenBytes` are in bytes.
- `percent` is a value between `0` and `1`.

Expand Down
64 changes: 54 additions & 10 deletions test/progress.js
Expand Up @@ -26,43 +26,87 @@ test('report progress', async t => {
const buffer = crypto.randomBytes(THREE_HUNDRED_KILO);
fs.writeFileSync(t.context.source, buffer);

let callCount = 0;
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
callCount++;
const progressHandler = progress => {
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);
};

let callCount = 0;

await cpFile(t.context.source, t.context.destination).on('progress', progress => {
callCount++;
progressHandler(progress);
});

t.true(callCount > 0);

let callCountOption = 0;

await cpFile(t.context.source, t.context.destination, {
onProgress: progress => {
callCountOption++;
progressHandler(progress);
}
});

t.true(callCountOption > 0);
});

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

let lastEvent;
let lastRecordEvent;

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

t.is(lastEvent.percent, 1);
t.is(lastEvent.writtenBytes, THREE_HUNDRED_KILO);
t.is(lastRecordEvent.percent, 1);
t.is(lastRecordEvent.writtenBytes, THREE_HUNDRED_KILO);

let lastRecordOption;

await cpFile(t.context.source, t.context.destination, {
onProgress: progress => {
lastRecordOption = progress;
}
});

t.is(lastRecordOption.percent, 1);
t.is(lastRecordOption.writtenBytes, THREE_HUNDRED_KILO);
});

test('report progress for empty files once', async t => {
fs.writeFileSync(t.context.source, '');

let callCount = 0;
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
callCount++;
const progressHandler = progress => {
t.is(progress.size, 0);
t.is(progress.writtenBytes, 0);
t.is(progress.percent, 1);
};

let callCount = 0;

await cpFile(t.context.source, t.context.destination).on('progress', progress => {
callCount++;
progressHandler(progress);
});

t.is(callCount, 1);

let callCountOption = 0;

await cpFile(t.context.source, t.context.destination, {
onProgress: progress => {
callCountOption++;
progressHandler(progress);
}
});

t.is(callCountOption, 1);
});

0 comments on commit cf322af

Please sign in to comment.