ssh2-sftp-client 7.0.4 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/README.org +22 -3
- package/package.json +6 -6
- package/src/index.js +39 -35
package/README.md
CHANGED
|
@@ -61,9 +61,9 @@ an SFTP client for node.js, a wrapper around [SSH2](https://github.com/mscdex/ss
|
|
|
61
61
|
|
|
62
62
|
Documentation on the methods and available options in the underlying modules can be found on the [SSH2](https://github.com/mscdex/ssh2) project pages.
|
|
63
63
|
|
|
64
|
-
Current stable release is **v7.0
|
|
64
|
+
Current stable release is **v7.1.0**.
|
|
65
65
|
|
|
66
|
-
Code has been tested against Node versions 12.22.
|
|
66
|
+
Code has been tested against Node versions 12.22.6, 14.17.6 and 16.10.0
|
|
67
67
|
|
|
68
68
|
Node versions < 10.x are not supported.
|
|
69
69
|
|
|
@@ -99,6 +99,8 @@ sftp.connect({
|
|
|
99
99
|
|
|
100
100
|
- **Breaking Change** Expanded option handling for `get()` and `put()` methods. A number of use cases were identified where setting specific options on the read and write streams and the pipe operation are necessary. For example, disabling `autoClose` on streams or the `end` event in pipes. The `options` argument for `get()` and `put()` calls now supports properties for `readStreamOptions`, `writeStreamOptions` and `pipeOptions`. Note that options are only applied to streams created by the `get()` and `put()` methods. Streams passed into these methods are under the control of the client code and therefore cannot have options supplied in arguments to those streams (you would apply such options when you create the streams). Options are typically only necessary in special use cases. Most of the time, no options are required. However, if you are currently using options to either `put()` or `get()`, you will need to update your code to map these options to the new structure.
|
|
101
101
|
|
|
102
|
+
- **Breaking Change 7.1.0** A race condition was identified when using a put() call with a writeStream option of `autoClose: false`. In some situations, the promise would be resolved before the final close of the write stream. This could result in errors if you immediately attempt to access the uploaded file. To avoid this situatioin, the promise is now resolved once a `close` event is emitted. This means that setting `autoClose: false` can no longer be supported. The write stream for `put()` will autoClose once data writing has completed.
|
|
103
|
+
|
|
102
104
|
- Improved event handling. A listener for a global error event is now defined to catch errors which occur in-between method calls i.e. connection lost in-between calls to the library methods. A new mechanism has also been added for removal of listeners when no longer required.
|
|
103
105
|
|
|
104
106
|
# Documentation<a id="sec-5"></a>
|
|
@@ -497,11 +499,12 @@ Upload data from local system to remote server. If the `src` argument is a strin
|
|
|
497
499
|
flags: 'w', // w - write and a - append
|
|
498
500
|
encoding: null, // use null for binary files
|
|
499
501
|
mode: 0o666, // mode to use for created file (rwx)
|
|
500
|
-
autoClose: true // automatically close the write stream when finished
|
|
501
502
|
}}
|
|
502
503
|
```
|
|
503
504
|
|
|
504
505
|
The most common options to use are mode and encoding. The values shown above are the defaults. You do not have to set encoding to utf-8 for text files, null is fine for all file types. However, using utf-8 encoding for binary files will often result in data corruption.
|
|
506
|
+
|
|
507
|
+
Note that you cannot set `autoClose: false` for `writeStreamOptions`. If you attempt to set this property to false, it will be ignored. This is necessary to avoid a race condition which may exist when setting `autoClose` to false on the writeStream. As there is no easy way to access the writeStream once the promise has been resolved, setting this to autoClose false is not terribly useful as there is no easy way to manually close the stream after the promise has been resolved.
|
|
505
508
|
|
|
506
509
|
2. Example Use
|
|
507
510
|
|
|
@@ -1356,3 +1359,5 @@ Thanks to the following for their contributions -
|
|
|
1356
1359
|
- **anton-erofeev:** Documentation fix
|
|
1357
1360
|
- **Ladislav Jacho:** Contributed solution explanation for connections hanging when transferring larger files.
|
|
1358
1361
|
- **Emma Milner:** Contributed fix for put() bug
|
|
1362
|
+
- **Witni Davis:** Contributed PR to fix put() RCE when using 'finish' rather than 'close' to resolve promise
|
|
1363
|
+
- **Maik Marschner:** Contributed fix for connect() not returning sftp object. Also included test to check for this regression in future.
|
package/README.org
CHANGED
|
@@ -9,9 +9,9 @@ convenience abstraction as well as a Promise based API.
|
|
|
9
9
|
Documentation on the methods and available options in the underlying modules can
|
|
10
10
|
be found on the [[https://github.com/mscdex/ssh2][SSH2]] project pages.
|
|
11
11
|
|
|
12
|
-
Current stable release is *v7.0
|
|
12
|
+
Current stable release is *v7.1.0*.
|
|
13
13
|
|
|
14
|
-
Code has been tested against Node versions 12.22.
|
|
14
|
+
Code has been tested against Node versions 12.22.6, 14.17.6 and 16.10.0
|
|
15
15
|
|
|
16
16
|
Node versions < 10.x are not supported.
|
|
17
17
|
|
|
@@ -62,6 +62,14 @@ npm install ssh2-sftp-client
|
|
|
62
62
|
currently using options to either ~put()~ or ~get()~, you will need to update
|
|
63
63
|
your code to map these options to the new structure.
|
|
64
64
|
|
|
65
|
+
- *Breaking Change 7.1.0* A race condition was identified when using a put()
|
|
66
|
+
call with a writeStream option of ~autoClose: false~. In some situations, the
|
|
67
|
+
promise would be resolved before the final close of the write stream. This
|
|
68
|
+
could result in errors if you immediately attempt to access the uploaded
|
|
69
|
+
file. To avoid this situatioin, the promise is now resolved once a ~close~
|
|
70
|
+
event is emitted. This means that setting ~autoClose: false~ can no longer be
|
|
71
|
+
supported. The write stream for ~put()~ will autoClose once data writing has completed.
|
|
72
|
+
|
|
65
73
|
- Improved event handling. A listener for a global error event is now defined to
|
|
66
74
|
catch errors which occur in-between method calls i.e. connection lost
|
|
67
75
|
in-between calls to the library methods. A new mechanism has also been added
|
|
@@ -546,7 +554,6 @@ option value. For example, you might use the following to set ~writeStream~ opti
|
|
|
546
554
|
flags: 'w', // w - write and a - append
|
|
547
555
|
encoding: null, // use null for binary files
|
|
548
556
|
mode: 0o666, // mode to use for created file (rwx)
|
|
549
|
-
autoClose: true // automatically close the write stream when finished
|
|
550
557
|
}}
|
|
551
558
|
#+end_src
|
|
552
559
|
|
|
@@ -555,6 +562,14 @@ the defaults. You do not have to set encoding to utf-8 for text files, null is
|
|
|
555
562
|
fine for all file types. However, using utf-8 encoding for binary files will
|
|
556
563
|
often result in data corruption.
|
|
557
564
|
|
|
565
|
+
Note that you cannot set ~autoClose: false~ for ~writeStreamOptions~. If you
|
|
566
|
+
attempt to set this property to false, it will be ignored. This is necessary to
|
|
567
|
+
avoid a race condition which may exist when setting ~autoClose~ to false on the
|
|
568
|
+
writeStream. As there is no easy way to access the writeStream once the promise
|
|
569
|
+
has been resolved, setting this to autoClose false is not terribly useful as
|
|
570
|
+
there is no easy way to manually close the stream after the promise has been
|
|
571
|
+
resolved.
|
|
572
|
+
|
|
558
573
|
**** Example Use
|
|
559
574
|
|
|
560
575
|
#+begin_src javascript
|
|
@@ -1765,3 +1780,7 @@ Thanks to the following for their contributions -
|
|
|
1765
1780
|
- Ladislav Jacho :: Contributed solution explanation for connections hanging
|
|
1766
1781
|
when transferring larger files.
|
|
1767
1782
|
- Emma Milner :: Contributed fix for put() bug
|
|
1783
|
+
- Witni Davis :: Contributed PR to fix put() RCE when using 'finish' rather than
|
|
1784
|
+
'close' to resolve promise
|
|
1785
|
+
- Maik Marschner :: Contributed fix for connect() not returning sftp object.
|
|
1786
|
+
Also included test to check for this regression in future.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ssh2-sftp-client",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"description": "ssh2 sftp client for node",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"repository": {
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"concat-stream": "^2.0.0",
|
|
34
34
|
"promise-retry": "^2.0.1",
|
|
35
|
-
"ssh2": "^1.
|
|
35
|
+
"ssh2": "^1.5.0"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"chai": "^4.
|
|
38
|
+
"chai": "^4.3.4",
|
|
39
39
|
"chai-as-promised": "^7.1.1",
|
|
40
40
|
"chai-subset": "^1.6.0",
|
|
41
41
|
"checksum": "^1.0.0",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"eslint-plugin-mocha": "^9.0.0",
|
|
46
46
|
"eslint-plugin-node": "^11.1.0",
|
|
47
47
|
"eslint-plugin-promise": "^5.1.0",
|
|
48
|
-
"eslint-plugin-unicorn": "^
|
|
49
|
-
"mocha": "^9.
|
|
48
|
+
"eslint-plugin-unicorn": "^36.0.0",
|
|
49
|
+
"mocha": "^9.1.2",
|
|
50
50
|
"moment": "^2.29.1",
|
|
51
51
|
"nyc": "^15.1.0",
|
|
52
|
-
"prettier": "^2.
|
|
52
|
+
"prettier": "^2.4.1",
|
|
53
53
|
"through2": "^4.0.2",
|
|
54
54
|
"winston": "^3.3.3"
|
|
55
55
|
}
|
package/src/index.js
CHANGED
|
@@ -114,7 +114,7 @@ class SftpClient {
|
|
|
114
114
|
*
|
|
115
115
|
* @param {Object} config - an SFTP configuration object
|
|
116
116
|
*
|
|
117
|
-
* @return {Promise} which will resolve to an sftp client object
|
|
117
|
+
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
118
118
|
*
|
|
119
119
|
*/
|
|
120
120
|
getConnection(config) {
|
|
@@ -173,7 +173,7 @@ class SftpClient {
|
|
|
173
173
|
*
|
|
174
174
|
* @param {Object} config - an SFTP configuration object
|
|
175
175
|
*
|
|
176
|
-
* @return {Promise} which will resolve to an sftp client object
|
|
176
|
+
* @return {Promise<Object>} which will resolve to an sftp client object
|
|
177
177
|
*
|
|
178
178
|
*/
|
|
179
179
|
async connect(config) {
|
|
@@ -204,7 +204,7 @@ class SftpClient {
|
|
|
204
204
|
minTimeout: config.retry_minTimeout || 1000,
|
|
205
205
|
}
|
|
206
206
|
);
|
|
207
|
-
|
|
207
|
+
return this.getSftpChannel();
|
|
208
208
|
} catch (err) {
|
|
209
209
|
this.debugMsg(`connect: Error ${err.message}`);
|
|
210
210
|
this._resetEventFlags();
|
|
@@ -221,7 +221,7 @@ class SftpClient {
|
|
|
221
221
|
* Returns undefined if the path does not exists.
|
|
222
222
|
*
|
|
223
223
|
* @param {String} remotePath - remote path, may be relative
|
|
224
|
-
* @returns {Promise} - remote absolute path or
|
|
224
|
+
* @returns {Promise<String>} - remote absolute path or ''
|
|
225
225
|
*/
|
|
226
226
|
realPath(remotePath) {
|
|
227
227
|
return new Promise((resolve, reject) => {
|
|
@@ -249,6 +249,13 @@ class SftpClient {
|
|
|
249
249
|
});
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
+
/**
|
|
253
|
+
* @async
|
|
254
|
+
*
|
|
255
|
+
* Return the current workding directory path
|
|
256
|
+
*
|
|
257
|
+
* @returns {Promise<String>} - current remote working directory
|
|
258
|
+
*/
|
|
252
259
|
cwd() {
|
|
253
260
|
return this.realPath('.');
|
|
254
261
|
}
|
|
@@ -257,7 +264,7 @@ class SftpClient {
|
|
|
257
264
|
* Retrieves attributes for path
|
|
258
265
|
*
|
|
259
266
|
* @param {String} remotePath - a string containing the path to a file
|
|
260
|
-
* @return {Promise} stats - attributes info
|
|
267
|
+
* @return {Promise<Object>} stats - attributes info
|
|
261
268
|
*/
|
|
262
269
|
async stat(remotePath) {
|
|
263
270
|
const _stat = (aPath) => {
|
|
@@ -323,7 +330,7 @@ class SftpClient {
|
|
|
323
330
|
*
|
|
324
331
|
* @param {string} remotePath - path to the object on the sftp server.
|
|
325
332
|
*
|
|
326
|
-
* @return {Promise} returns false if object does not exist. Returns type of
|
|
333
|
+
* @return {Promise<Boolean|String>} returns false if object does not exist. Returns type of
|
|
327
334
|
* object if it does
|
|
328
335
|
*/
|
|
329
336
|
async exists(remotePath) {
|
|
@@ -382,8 +389,7 @@ class SftpClient {
|
|
|
382
389
|
*
|
|
383
390
|
* @param {String} remotePath - path to remote directory
|
|
384
391
|
* @param {RegExp} pattern - regular expression to match filenames
|
|
385
|
-
* @returns {Promise} array of file description objects
|
|
386
|
-
* @throws {Error}
|
|
392
|
+
* @returns {Promise<Array>} array of file description objects
|
|
387
393
|
*/
|
|
388
394
|
list(remotePath, pattern = /.*/) {
|
|
389
395
|
return new Promise((resolve, reject) => {
|
|
@@ -449,7 +455,7 @@ class SftpClient {
|
|
|
449
455
|
* @param {Object} options - options object with supported properties of readStreamOptions,
|
|
450
456
|
* writeStreamOptions and pipeOptions.
|
|
451
457
|
*
|
|
452
|
-
* @return {Promise}
|
|
458
|
+
* @return {Promise<String|Stream|Buffer>}
|
|
453
459
|
*/
|
|
454
460
|
get(
|
|
455
461
|
remotePath,
|
|
@@ -549,7 +555,7 @@ class SftpClient {
|
|
|
549
555
|
* @param {String} remotePath
|
|
550
556
|
* @param {String} localPath
|
|
551
557
|
* @param {Object} options
|
|
552
|
-
* @return {Promise} the result of downloading the file
|
|
558
|
+
* @return {Promise<String>} the result of downloading the file
|
|
553
559
|
*/
|
|
554
560
|
async fastGet(remotePath, localPath, options) {
|
|
555
561
|
try {
|
|
@@ -569,7 +575,7 @@ class SftpClient {
|
|
|
569
575
|
err.code = errorCode.badPath;
|
|
570
576
|
throw err;
|
|
571
577
|
}
|
|
572
|
-
await new Promise((resolve, reject) => {
|
|
578
|
+
let rslt = await new Promise((resolve, reject) => {
|
|
573
579
|
if (haveConnection(this, 'fastGet', reject)) {
|
|
574
580
|
this.debugMsg(
|
|
575
581
|
`fastGet -> remote: ${remotePath} local: ${localPath} `,
|
|
@@ -587,6 +593,7 @@ class SftpClient {
|
|
|
587
593
|
}).finally(() => {
|
|
588
594
|
removeTempListeners(this, 'fastGet');
|
|
589
595
|
});
|
|
596
|
+
return rslt;
|
|
590
597
|
} catch (err) {
|
|
591
598
|
this._resetEventFlags();
|
|
592
599
|
throw fmtError(err, 'fastGet');
|
|
@@ -604,7 +611,7 @@ class SftpClient {
|
|
|
604
611
|
* @param {String} localPath
|
|
605
612
|
* @param {String} remotePath
|
|
606
613
|
* @param {Object} options
|
|
607
|
-
* @return {Promise} the result of downloading the file
|
|
614
|
+
* @return {Promise<String>} the result of downloading the file
|
|
608
615
|
*/
|
|
609
616
|
fastPut(localPath, remotePath, options) {
|
|
610
617
|
this.debugMsg(`fastPut -> local ${localPath} remote ${remotePath}`);
|
|
@@ -664,12 +671,16 @@ class SftpClient {
|
|
|
664
671
|
* @param {Object} options - options used for read, write stream and pipe configuration
|
|
665
672
|
* value supported by node. Allowed properties are readStreamOptions,
|
|
666
673
|
* writeStreamOptions and pipeOptions.
|
|
667
|
-
* @return {Promise}
|
|
674
|
+
* @return {Promise<String>}
|
|
668
675
|
*/
|
|
669
676
|
put(
|
|
670
677
|
localSrc,
|
|
671
678
|
remotePath,
|
|
672
|
-
options = {
|
|
679
|
+
options = {
|
|
680
|
+
readStreamOptions: {},
|
|
681
|
+
writeStreamOptions: { autoClose: true },
|
|
682
|
+
pipeOptions: {},
|
|
683
|
+
}
|
|
673
684
|
) {
|
|
674
685
|
let wtr, rdr;
|
|
675
686
|
|
|
@@ -691,13 +702,15 @@ class SftpClient {
|
|
|
691
702
|
addTempListeners(this, 'put', reject);
|
|
692
703
|
wtr = this.sftp.createWriteStream(
|
|
693
704
|
remotePath,
|
|
694
|
-
options.writeStreamOptions
|
|
705
|
+
options.writeStreamOptions
|
|
706
|
+
? { ...options.writeStreamOptions, autoClose: true }
|
|
707
|
+
: {}
|
|
695
708
|
);
|
|
696
709
|
wtr.once('error', (err) => {
|
|
697
710
|
this.debugMsg(`put: write stream error ${err.message}`);
|
|
698
711
|
reject(fmtError(`${err.message} ${remotePath}`, 'put', err.code));
|
|
699
712
|
});
|
|
700
|
-
wtr.once('
|
|
713
|
+
wtr.once('close', () => {
|
|
701
714
|
this.debugMsg('put: promise resolved');
|
|
702
715
|
resolve(`Uploaded data stream to ${remotePath}`);
|
|
703
716
|
});
|
|
@@ -741,13 +754,6 @@ class SftpClient {
|
|
|
741
754
|
) {
|
|
742
755
|
rdr.destroy();
|
|
743
756
|
}
|
|
744
|
-
if (
|
|
745
|
-
wtr &&
|
|
746
|
-
options.writeStreamOptions &&
|
|
747
|
-
options.writeStreamOptions.autoClose === false
|
|
748
|
-
) {
|
|
749
|
-
wtr.destroy();
|
|
750
|
-
}
|
|
751
757
|
});
|
|
752
758
|
}
|
|
753
759
|
|
|
@@ -757,9 +763,8 @@ class SftpClient {
|
|
|
757
763
|
* @param {Buffer|stream} input
|
|
758
764
|
* @param {String} remotePath
|
|
759
765
|
* @param {Object} options
|
|
760
|
-
* @return {Promise}
|
|
766
|
+
* @return {Promise<String>}
|
|
761
767
|
*/
|
|
762
|
-
|
|
763
768
|
async append(input, remotePath, options = {}) {
|
|
764
769
|
const fileType = await this.exists(remotePath);
|
|
765
770
|
if (fileType && fileType === 'd') {
|
|
@@ -807,7 +812,7 @@ class SftpClient {
|
|
|
807
812
|
*
|
|
808
813
|
* @param {string} remotePath - remote directory path.
|
|
809
814
|
* @param {boolean} recursive - if true, recursively create directories
|
|
810
|
-
* @return {Promise}
|
|
815
|
+
* @return {Promise<String>}
|
|
811
816
|
*/
|
|
812
817
|
async mkdir(remotePath, recursive = false) {
|
|
813
818
|
const _mkdir = (p) => {
|
|
@@ -873,7 +878,7 @@ class SftpClient {
|
|
|
873
878
|
* @param {string} remotePath - path to directory to be removed
|
|
874
879
|
* @param {boolean} recursive - if true, remove directories/files in target
|
|
875
880
|
* directory
|
|
876
|
-
* @return {Promise}
|
|
881
|
+
* @return {Promise<String>}
|
|
877
882
|
*/
|
|
878
883
|
async rmdir(remotePath, recursive = false) {
|
|
879
884
|
const _rmdir = (p) => {
|
|
@@ -926,7 +931,7 @@ class SftpClient {
|
|
|
926
931
|
* @param {string} remotePath - path to the file to delete
|
|
927
932
|
* @param {boolean} notFoundOK - if true, ignore errors for missing target.
|
|
928
933
|
* Default is false.
|
|
929
|
-
* @return {Promise} with string 'Successfully deleted file' once resolved
|
|
934
|
+
* @return {Promise<String>} with string 'Successfully deleted file' once resolved
|
|
930
935
|
*
|
|
931
936
|
*/
|
|
932
937
|
delete(remotePath, notFoundOK = false) {
|
|
@@ -963,7 +968,7 @@ class SftpClient {
|
|
|
963
968
|
* @param {string} fromPath - path to the file to be renamed.
|
|
964
969
|
* @param {string} toPath - path to the new name.
|
|
965
970
|
*
|
|
966
|
-
* @return {Promise}
|
|
971
|
+
* @return {Promise<String>}
|
|
967
972
|
*
|
|
968
973
|
*/
|
|
969
974
|
rename(fromPath, toPath) {
|
|
@@ -1000,7 +1005,7 @@ class SftpClient {
|
|
|
1000
1005
|
* @param {string} fromPath - path to the file to be renamed.
|
|
1001
1006
|
* @param {string} toPath - path the new name.
|
|
1002
1007
|
*
|
|
1003
|
-
* @return {Promise}
|
|
1008
|
+
* @return {Promise<String>}
|
|
1004
1009
|
*
|
|
1005
1010
|
*/
|
|
1006
1011
|
posixRename(fromPath, toPath) {
|
|
@@ -1036,7 +1041,7 @@ class SftpClient {
|
|
|
1036
1041
|
* @param {string} remotePath - path to the remote target object.
|
|
1037
1042
|
* @param {number | string} mode - the new octal mode to set
|
|
1038
1043
|
*
|
|
1039
|
-
* @return {Promise}
|
|
1044
|
+
* @return {Promise<String>}
|
|
1040
1045
|
*/
|
|
1041
1046
|
chmod(remotePath, mode) {
|
|
1042
1047
|
return new Promise((resolve, reject) => {
|
|
@@ -1064,8 +1069,7 @@ class SftpClient {
|
|
|
1064
1069
|
* @param {String} dstDir - remote destination directory
|
|
1065
1070
|
* @param {RegExp} filter - (Optional) a regular expression used to select
|
|
1066
1071
|
* files and directories to upload
|
|
1067
|
-
* @returns {String}
|
|
1068
|
-
* @throws {Error}
|
|
1072
|
+
* @returns {Promise<String>}
|
|
1069
1073
|
*/
|
|
1070
1074
|
async uploadDir(srcDir, dstDir, filter = /.*/) {
|
|
1071
1075
|
try {
|
|
@@ -1124,8 +1128,7 @@ class SftpClient {
|
|
|
1124
1128
|
* @param {String} dstDir - local destination directory
|
|
1125
1129
|
* @param {RegExp} filter - (Optional) a regular expression used to select
|
|
1126
1130
|
* files and directories to upload
|
|
1127
|
-
* @returns {Promise}
|
|
1128
|
-
* @throws {Error}
|
|
1131
|
+
* @returns {Promise<String>}
|
|
1129
1132
|
*/
|
|
1130
1133
|
async downloadDir(srcDir, dstDir, filter = /.*/) {
|
|
1131
1134
|
try {
|
|
@@ -1176,6 +1179,7 @@ class SftpClient {
|
|
|
1176
1179
|
*
|
|
1177
1180
|
* End the SFTP connection
|
|
1178
1181
|
*
|
|
1182
|
+
* @returns {Promise<Boolean>}
|
|
1179
1183
|
*/
|
|
1180
1184
|
end() {
|
|
1181
1185
|
let endCloseHandler;
|