Skip to content

Commit cf322af

Browse files
fitiskinsindresorhus
andauthoredJun 21, 2022
Add onProgress option (#52)
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
1 parent 3fe6ab4 commit cf322af

7 files changed

+158
-60
lines changed
 

‎index.d.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ declare namespace cpFile {
2626
readonly cwd?: string;
2727
}
2828

29+
interface AsyncOptions {
30+
/**
31+
The given function is called whenever there is measurable progress.
32+
33+
Note: For empty files, the `onProgress` event is emitted only once.
34+
35+
@example
36+
```
37+
import cpFile = require('cp-file');
38+
39+
(async () => {
40+
await cpFile('source/unicorn.png', 'destination/unicorn.png', {
41+
onProgress: progress => {
42+
// ...
43+
}
44+
});
45+
})();
46+
```
47+
*/
48+
readonly onProgress?: (progress: ProgressData) => void;
49+
}
50+
2951
interface ProgressData {
3052
/**
3153
Absolute path to source.
@@ -55,9 +77,11 @@ declare namespace cpFile {
5577

5678
interface ProgressEmitter {
5779
/**
80+
@deprecated Use `onProgress` option instead.
81+
5882
Note: For empty files, the `progress` event is emitted only once.
5983
*/
60-
on(event: 'progress', handler: (data: ProgressData) => void): Promise<void>;
84+
on(event: 'progress', handler: AsyncOptions['onProgress']): Promise<void>;
6185
}
6286
}
6387

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

84108
/**
85109
Copy a file synchronously.

‎index.js

+21-10
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,31 @@ const {constants: fsConstants} = require('fs');
44
const pEvent = require('p-event');
55
const CpFileError = require('./cp-file-error');
66
const fs = require('./fs');
7-
const ProgressEmitter = require('./progress-emitter');
87

9-
const cpFileAsync = async (source, destination, options, progressEmitter) => {
8+
const cpFileAsync = async (source, destination, options) => {
109
let readError;
11-
const stat = await fs.stat(source);
12-
progressEmitter.size = stat.size;
10+
const {size} = await fs.stat(source);
1311

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

16+
const emitProgress = writtenBytes => {
17+
if (typeof options.onProgress !== 'function') {
18+
return;
19+
}
20+
21+
options.onProgress({
22+
sourcePath: path.resolve(source),
23+
destinationPath: path.resolve(destination),
24+
size,
25+
writtenBytes,
26+
percent: writtenBytes === size ? 1 : writtenBytes / size
27+
});
28+
};
29+
1830
readStream.on('data', () => {
19-
progressEmitter.writtenBytes = writeStream.bytesWritten;
31+
emitProgress(writeStream.bytesWritten);
2032
});
2133

2234
readStream.once('error', error => {
@@ -32,7 +44,7 @@ const cpFileAsync = async (source, destination, options, progressEmitter) => {
3244
const writePromise = pEvent(writeStream, 'close');
3345
readStream.pipe(writeStream);
3446
await writePromise;
35-
progressEmitter.writtenBytes = progressEmitter.size;
47+
emitProgress(size);
3648
shouldUpdateStats = true;
3749
} catch (error) {
3850
throw new CpFileError(`Cannot write to \`${destination}\`: ${error.message}`, error);
@@ -76,11 +88,10 @@ const cpFile = (sourcePath, destinationPath, options = {}) => {
7688
...options
7789
};
7890

79-
const progressEmitter = new ProgressEmitter(path.resolve(sourcePath), path.resolve(destinationPath));
80-
const promise = cpFileAsync(sourcePath, destinationPath, options, progressEmitter);
91+
const promise = cpFileAsync(sourcePath, destinationPath, options);
8192

82-
promise.on = (...arguments_) => {
83-
progressEmitter.on(...arguments_);
93+
promise.on = (_eventName, callback) => {
94+
options.onProgress = callback;
8495
return promise;
8596
};
8697

‎index.test-d.ts

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,19 @@ expectError(
1818
directoryMode: '700'
1919
})
2020
);
21+
expectType<Promise<void> & ProgressEmitter>(
22+
cpFile('source/unicorn.png', 'destination/unicorn.png', {
23+
onProgress: progress => {
24+
expectType<ProgressData>(progress);
25+
26+
expectType<string>(progress.sourcePath);
27+
expectType<string>(progress.destinationPath);
28+
expectType<number>(progress.size);
29+
expectType<number>(progress.writtenBytes);
30+
expectType<number>(progress.percent);
31+
}
32+
})
33+
);
2134
expectType<Promise<void>>(
2235
cpFile('source/unicorn.png', 'destination/unicorn.png').on(
2336
'progress',

‎package.json

+1-2
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@
2020
"cp-file-error.js",
2121
"fs.js",
2222
"index.js",
23-
"index.d.ts",
24-
"progress-emitter.js"
23+
"index.d.ts"
2524
],
2625
"keywords": [
2726
"copy",

‎progress-emitter.js

-35
This file was deleted.

‎readme.md

+43-1
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,50 @@ Default: `0o777`
7777

7878
It has no effect on Windows.
7979

80+
##### onProgress
81+
82+
Type: `(progress: ProgressData) => void`
83+
84+
The given function is called whenever there is measurable progress.
85+
86+
Only available when using the async method.
87+
88+
###### `ProgressData`
89+
90+
```js
91+
{
92+
sourcePath: string,
93+
destinationPath: string,
94+
size: number,
95+
writtenBytes: number,
96+
percent: number
97+
}
98+
```
99+
100+
- `sourcePath` and `destinationPath` are absolute paths.
101+
- `size` and `writtenBytes` are in bytes.
102+
- `percent` is a value between `0` and `1`.
103+
104+
###### Notes
105+
106+
- For empty files, the `onProgress` callback function is emitted only once.
107+
108+
```js
109+
const cpFile = require('cp-file');
110+
111+
(async () => {
112+
await cpFile(source, destination, {
113+
onProgress: progress => {
114+
//
115+
}
116+
});
117+
})();
118+
```
119+
80120
### cpFile.on('progress', handler)
81121

122+
> Deprecated. Use `onProgress` option instead.
123+
82124
Progress reporting. Only available when using the async method.
83125

84126
#### handler(data)
@@ -97,7 +139,7 @@ Type: `Function`
97139
}
98140
```
99141

100-
- `source` and `destination` are absolute paths.
142+
- `sourcePath` and `destinationPath` are absolute paths.
101143
- `size` and `writtenBytes` are in bytes.
102144
- `percent` is a value between `0` and `1`.
103145

‎test/progress.js

+54-10
Original file line numberDiff line numberDiff line change
@@ -26,43 +26,87 @@ test('report progress', async t => {
2626
const buffer = crypto.randomBytes(THREE_HUNDRED_KILO);
2727
fs.writeFileSync(t.context.source, buffer);
2828

29-
let callCount = 0;
30-
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
31-
callCount++;
29+
const progressHandler = progress => {
3230
t.is(typeof progress.sourcePath, 'string');
3331
t.is(typeof progress.destinationPath, 'string');
3432
t.is(typeof progress.size, 'number');
3533
t.is(typeof progress.writtenBytes, 'number');
3634
t.is(typeof progress.percent, 'number');
3735
t.is(progress.size, THREE_HUNDRED_KILO);
36+
};
37+
38+
let callCount = 0;
39+
40+
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
41+
callCount++;
42+
progressHandler(progress);
3843
});
3944

4045
t.true(callCount > 0);
46+
47+
let callCountOption = 0;
48+
49+
await cpFile(t.context.source, t.context.destination, {
50+
onProgress: progress => {
51+
callCountOption++;
52+
progressHandler(progress);
53+
}
54+
});
55+
56+
t.true(callCountOption > 0);
4157
});
4258

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

47-
let lastEvent;
63+
let lastRecordEvent;
64+
4865
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
49-
lastEvent = progress;
66+
lastRecordEvent = progress;
5067
});
5168

52-
t.is(lastEvent.percent, 1);
53-
t.is(lastEvent.writtenBytes, THREE_HUNDRED_KILO);
69+
t.is(lastRecordEvent.percent, 1);
70+
t.is(lastRecordEvent.writtenBytes, THREE_HUNDRED_KILO);
71+
72+
let lastRecordOption;
73+
74+
await cpFile(t.context.source, t.context.destination, {
75+
onProgress: progress => {
76+
lastRecordOption = progress;
77+
}
78+
});
79+
80+
t.is(lastRecordOption.percent, 1);
81+
t.is(lastRecordOption.writtenBytes, THREE_HUNDRED_KILO);
5482
});
5583

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

59-
let callCount = 0;
60-
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
61-
callCount++;
87+
const progressHandler = progress => {
6288
t.is(progress.size, 0);
6389
t.is(progress.writtenBytes, 0);
6490
t.is(progress.percent, 1);
91+
};
92+
93+
let callCount = 0;
94+
95+
await cpFile(t.context.source, t.context.destination).on('progress', progress => {
96+
callCount++;
97+
progressHandler(progress);
6598
});
6699

67100
t.is(callCount, 1);
101+
102+
let callCountOption = 0;
103+
104+
await cpFile(t.context.source, t.context.destination, {
105+
onProgress: progress => {
106+
callCountOption++;
107+
progressHandler(progress);
108+
}
109+
});
110+
111+
t.is(callCountOption, 1);
68112
});

0 commit comments

Comments
 (0)
Please sign in to comment.