zip-lib 0.5.0 → 0.7.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # zip-lib
2
2
  zip and unzip library for node.
3
3
 
4
- [![npm Package](https://img.shields.io/npm/v/zip-lib.svg)](https://www.npmjs.org/package/zip-lib) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fpsqdb/zip-lib/blob/master/LICENSE) ![node](https://img.shields.io/node/v/zip-lib) [![Build Status](https://travis-ci.org/fpsqdb/zip-lib.svg?branch=master)](https://travis-ci.org/fpsqdb/zip-lib)
4
+ [![npm Package](https://img.shields.io/npm/v/zip-lib.svg)](https://www.npmjs.org/package/zip-lib) [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fpsqdb/zip-lib/blob/master/LICENSE) ![node](https://img.shields.io/node/v/zip-lib) ![Node.js CI](https://github.com/fpsqdb/zip-lib/workflows/Node.js%20CI/badge.svg)
5
5
 
6
6
  ## Install
7
7
 
@@ -350,11 +350,10 @@ Object
350
350
 
351
351
  Object
352
352
  - `overwrite?`: String (optional) - If it is true, the target directory will be deleted before extract. The default value is `false`.
353
- - `symlinkAsFileOnWindows?`: Boolean (optional) - Extract symbolic links as files on Windows. This value is only available on Windows and ignored on other platforms. The default value is `true`.
353
+ - `symlinkAsFileOnWindows?`: Boolean (optional) - Extract symbolic links as files on Windows. This value is only available on Windows and ignored on other platforms. The default value is `true`.<br>If `true`, the symlink in the zip will be extracted as a normal file on Windows.<br>If `false`, the symlink in the zip will be extracted as a symlink correctly on Windows, but an `EPERM` error will be thrown under non-administrators.
354
354
 
355
- > On Windows, the default security policy allows only administrators to create symbolic links.
355
+ > ⚠**WARNING:** On Windows, the default security policy allows only administrators to create symbolic links. If you set `symlinkAsFileOnWindows` to `false` and the zip contains symlink, be sure to run the code under the administrator, otherwise an `EPERM` error will be thrown.
356
356
 
357
- If `true`, the symlink in the zip will be extracted as a normal file on Windows.<br>If `false`, the symlink in the zip will be extracted as a symlink correctly on Windows, but an `EPERM` error will be thrown under non-administrators.
358
357
  - `onEntry?`: Function (optional) - Called before an item is extracted.<br>Arguments:
359
358
  - `event`: Object - Represents an event that an entry is about to be extracted.
360
359
  - `entryName`: String (readonly) - Entry name.
@@ -4,13 +4,24 @@ export interface ICancelable {
4
4
  */
5
5
  cancel(): void;
6
6
  }
7
- export declare abstract class Cancelable implements ICancelable {
7
+ export declare class CancellationToken {
8
+ private _isCancelled;
9
+ private _callbacks;
8
10
  /**
9
- *
11
+ * A flag signalling is cancellation has been requested.
12
+ */
13
+ get isCancelled(): boolean;
14
+ /**
15
+ * Subscribe a callback when cancellation is requested. The callback
16
+ * only ever fires `once` as cancellation can only happen once.
17
+ * @param cb A function will be called whent cancellation is requested.
18
+ * @returns A function that Unsubscribe the cancellation callback.
10
19
  */
11
- constructor();
12
- protected isCanceled: boolean;
20
+ onCancelled(cb: () => void): () => void;
13
21
  cancel(): void;
22
+ }
23
+ export declare abstract class Cancelable implements ICancelable {
24
+ abstract cancel(): void;
14
25
  /**
15
26
  * Ignore any other error if the `cancel` method has been called
16
27
  *
@@ -19,7 +30,7 @@ export declare abstract class Cancelable implements ICancelable {
19
30
  * see https://travis-ci.org/fpsqdb/zip-lib/jobs/606040627#L124
20
31
  * @param error
21
32
  */
22
- protected wrapError(error: Error): Error;
33
+ protected wrapError(error: Error, isCanceled: boolean): Error;
23
34
  /**
24
35
  * Returns an error that signals cancellation.
25
36
  */
package/lib/cancelable.js CHANGED
@@ -1,15 +1,44 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- class Cancelable {
3
+ exports.Cancelable = exports.CancellationToken = void 0;
4
+ class CancellationToken {
5
+ constructor() {
6
+ this._isCancelled = false;
7
+ this._callbacks = new Set();
8
+ }
4
9
  /**
5
- *
10
+ * A flag signalling is cancellation has been requested.
6
11
  */
7
- constructor() {
8
- this.isCanceled = false;
12
+ get isCancelled() {
13
+ return this._isCancelled;
14
+ }
15
+ /**
16
+ * Subscribe a callback when cancellation is requested. The callback
17
+ * only ever fires `once` as cancellation can only happen once.
18
+ * @param cb A function will be called whent cancellation is requested.
19
+ * @returns A function that Unsubscribe the cancellation callback.
20
+ */
21
+ onCancelled(cb) {
22
+ if (this.isCancelled) {
23
+ cb();
24
+ return () => {
25
+ // noop
26
+ };
27
+ }
28
+ this._callbacks.add(cb);
29
+ return () => this._callbacks.delete(cb);
9
30
  }
10
31
  cancel() {
11
- this.isCanceled = true;
32
+ if (this._isCancelled) {
33
+ return;
34
+ }
35
+ this._isCancelled = true;
36
+ this._callbacks.forEach((cb) => cb());
37
+ this._callbacks.clear();
12
38
  }
39
+ }
40
+ exports.CancellationToken = CancellationToken;
41
+ class Cancelable {
13
42
  /**
14
43
  * Ignore any other error if the `cancel` method has been called
15
44
  *
@@ -18,8 +47,8 @@ class Cancelable {
18
47
  * see https://travis-ci.org/fpsqdb/zip-lib/jobs/606040627#L124
19
48
  * @param error
20
49
  */
21
- wrapError(error) {
22
- if (this.isCanceled) {
50
+ wrapError(error, isCanceled) {
51
+ if (isCanceled) {
23
52
  return this.canceledError();
24
53
  }
25
54
  return error;
@@ -28,10 +57,9 @@ class Cancelable {
28
57
  * Returns an error that signals cancellation.
29
58
  */
30
59
  canceledError() {
31
- let error = new Error("Canceled");
60
+ const error = new Error("Canceled");
32
61
  error.name = error.message;
33
62
  return error;
34
63
  }
35
64
  }
36
65
  exports.Cancelable = Cancelable;
37
- //# sourceMappingURL=cancelable.js.map
package/lib/fs.js CHANGED
@@ -1,16 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRootPath = exports.rimraf = exports.pathExists = exports.ensureFolder = exports.getFileEntry = exports.readdirp = void 0;
3
4
  const path = require("path");
4
5
  const util = require("./util");
5
6
  async function readdirp(folder) {
6
- let result = [];
7
+ const result = [];
7
8
  const files = await util.readdir(folder);
8
- for (let i = 0; i < files.length; i++) {
9
- const file = path.join(folder, files[i]);
10
- const stat = await util.lstat(file);
11
- let fileType = "file";
12
- if (stat.isDirectory()) {
13
- fileType = "dir";
9
+ for (const item of files) {
10
+ const file = path.join(folder, item);
11
+ const entry = await getFileEntry(file);
12
+ if (!entry.isSymbolicLink && entry.type === "dir") {
14
13
  const subFiles = await readdirp(file);
15
14
  if (subFiles.length > 0) {
16
15
  result.push(...subFiles);
@@ -19,21 +18,39 @@ async function readdirp(folder) {
19
18
  continue;
20
19
  }
21
20
  }
22
- else {
23
- if (stat.isSymbolicLink()) {
24
- fileType = "symlink";
25
- }
26
- }
27
- result.push({
28
- path: file,
29
- type: fileType,
30
- mtime: stat.mtime,
31
- mode: stat.mode
32
- });
21
+ result.push(entry);
33
22
  }
34
23
  return result;
35
24
  }
36
25
  exports.readdirp = readdirp;
26
+ async function getFileEntry(target) {
27
+ const stat = await util.lstat(target);
28
+ let isSymbolicLink = false;
29
+ let fileType = "file";
30
+ if (stat.isDirectory()) {
31
+ fileType = "dir";
32
+ }
33
+ else {
34
+ if (stat.isSymbolicLink()) {
35
+ isSymbolicLink = true;
36
+ // If the path is a link, we must instead use fs.stat() to find out if the
37
+ // link is a directory or not because lstat will always return the stat of
38
+ // the link which is always a file.
39
+ const actualStat = await util.stat(target);
40
+ if (actualStat.isDirectory()) {
41
+ fileType = "dir";
42
+ }
43
+ }
44
+ }
45
+ return {
46
+ path: target,
47
+ isSymbolicLink,
48
+ type: fileType,
49
+ mtime: stat.mtime,
50
+ mode: stat.mode
51
+ };
52
+ }
53
+ exports.getFileEntry = getFileEntry;
37
54
  async function ensureFolder(folder) {
38
55
  // stop at root
39
56
  if (folder === path.dirname(folder)) {
@@ -54,9 +71,9 @@ async function ensureFolder(folder) {
54
71
  }
55
72
  }
56
73
  exports.ensureFolder = ensureFolder;
57
- async function pathExists(path) {
74
+ async function pathExists(target) {
58
75
  try {
59
- await util.access(path);
76
+ await util.access(target);
60
77
  return true;
61
78
  }
62
79
  catch (error) {
@@ -101,14 +118,14 @@ async function mkdir(folder) {
101
118
  await util.mkdir(folder, 0o777);
102
119
  }
103
120
  catch (error) {
104
- // ENOENT: a parent folder does not exist yet
121
+ // ENOENT: a parent folder does not exist yet or folder name is invalid.
105
122
  if (error.code === "ENOENT") {
106
123
  return Promise.reject(error);
107
124
  }
108
125
  // Any other error: check if folder exists and
109
126
  // return normally in that case if its a folder
110
127
  try {
111
- const fileStat = await util.lstat(folder);
128
+ const fileStat = await util.stat(folder);
112
129
  if (!fileStat.isDirectory()) {
113
130
  return Promise.reject(new Error(`"${folder}" exists and is not a directory.`));
114
131
  }
@@ -137,23 +154,22 @@ function isDriveLetter(char0) {
137
154
  }
138
155
  const winSep = "\\";
139
156
  const unixSep = "/";
140
- function isRootPath(path) {
141
- if (!path) {
157
+ function isRootPath(target) {
158
+ if (!target) {
142
159
  return false;
143
160
  }
144
- if (path === winSep ||
145
- path === unixSep) {
161
+ if (target === winSep ||
162
+ target === unixSep) {
146
163
  return true;
147
164
  }
148
165
  if (process.platform === "win32") {
149
- if (path.length > 3) {
166
+ if (target.length > 3) {
150
167
  return false;
151
168
  }
152
- return isDriveLetter(path.charCodeAt(0))
153
- && path.charCodeAt(1) === charColon
154
- && (path.length === 2 || path.charCodeAt(2) === charWinSep || path.charCodeAt(2) === cahrUnixSep);
169
+ return isDriveLetter(target.charCodeAt(0))
170
+ && target.charCodeAt(1) === charColon
171
+ && (target.length === 2 || target.charCodeAt(2) === charWinSep || target.charCodeAt(2) === cahrUnixSep);
155
172
  }
156
173
  return false;
157
174
  }
158
175
  exports.isRootPath = isRootPath;
159
- //# sourceMappingURL=fs.js.map
package/lib/index.js CHANGED
@@ -1,12 +1,20 @@
1
1
  "use strict";
2
- function __export(m) {
3
- for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
4
- }
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
10
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
11
+ };
5
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.extract = exports.archiveFolder = exports.archiveFile = void 0;
6
14
  const zip_1 = require("./zip");
7
15
  const unzip_1 = require("./unzip");
8
- __export(require("./zip"));
9
- __export(require("./unzip"));
16
+ __exportStar(require("./zip"), exports);
17
+ __exportStar(require("./unzip"), exports);
10
18
  /**
11
19
  * Compress a single file to zip.
12
20
  * @param file
@@ -42,4 +50,3 @@ function extract(zipFile, targetFolder, options) {
42
50
  return unzip.extract(zipFile, targetFolder);
43
51
  }
44
52
  exports.extract = extract;
45
- //# sourceMappingURL=index.js.map
package/lib/unzip.d.ts CHANGED
@@ -9,11 +9,13 @@ export interface IExtractOptions {
9
9
  * Extract symbolic links as files on Windows. This value is only available on Windows and ignored on other platforms.
10
10
  * The default value is `true`.
11
11
  *
12
- * > On Windows, the default security policy allows only administrators to create symbolic links.
13
- *
14
12
  * If `true`, the symlink in the zip will be extracted as a normal file on Windows.
15
13
  *
16
14
  * If `false`, the symlink in the zip will be extracted as a symlink correctly on Windows, but an `EPERM` error will be thrown under non-administrators.
15
+ *
16
+ * > ⚠**WARNING:** On Windows, the default security policy allows only administrators to create symbolic links.
17
+ * If you set `symlinkAsFileOnWindows` to `false` and the zip contains symlink,
18
+ * be sure to run the code under the administrator, otherwise an `EPERM` error will be thrown.
17
19
  */
18
20
  symlinkAsFileOnWindows?: boolean;
19
21
  /**
@@ -49,7 +51,7 @@ export declare class Unzip extends Cancelable {
49
51
  */
50
52
  constructor(options?: IExtractOptions | undefined);
51
53
  private zipFile;
52
- private cancelCallback?;
54
+ private token;
53
55
  /**
54
56
  * Extract the zip file to the specified location.
55
57
  * @param zipFile
package/lib/unzip.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Unzip = void 0;
3
4
  const yauzl = require("yauzl");
4
5
  const exfs = require("./fs");
5
6
  const fs_1 = require("fs");
@@ -33,6 +34,50 @@ class EntryEvent {
33
34
  this._isPrevented = false;
34
35
  }
35
36
  }
37
+ class EntryContext {
38
+ constructor(_targetFolder, _realTargetFolder, symlinkAsFileOnWindows) {
39
+ this._targetFolder = _targetFolder;
40
+ this._realTargetFolder = _realTargetFolder;
41
+ this.symlinkAsFileOnWindows = symlinkAsFileOnWindows;
42
+ this._symlinkFileNames = [];
43
+ }
44
+ get decodeEntryFileName() {
45
+ return this._decodeEntryFileName;
46
+ }
47
+ set decodeEntryFileName(name) {
48
+ this._decodeEntryFileName = name;
49
+ }
50
+ get targetFolder() {
51
+ return this._targetFolder;
52
+ }
53
+ get realTargetFolder() {
54
+ return this._realTargetFolder;
55
+ }
56
+ get symlinkFileNames() {
57
+ return this._symlinkFileNames;
58
+ }
59
+ getFilePath() {
60
+ return path.join(this.targetFolder, this.decodeEntryFileName);
61
+ }
62
+ async isOutsideTargetFolder(tpath) {
63
+ if (this.symlinkFileNames.length === 0) {
64
+ return false;
65
+ }
66
+ if (process.platform === "win32" &&
67
+ this.symlinkAsFileOnWindows) {
68
+ return false;
69
+ }
70
+ for (const fileName of this.symlinkFileNames) {
71
+ if (tpath.includes(fileName)) {
72
+ const realFilePath = await util.realpath(tpath);
73
+ if (realFilePath.indexOf(this.realTargetFolder) !== 0) {
74
+ return true;
75
+ }
76
+ }
77
+ }
78
+ return false;
79
+ }
80
+ }
36
81
  /**
37
82
  * Extract the zip file.
38
83
  */
@@ -52,37 +97,48 @@ class Unzip extends cancelable_1.Cancelable {
52
97
  */
53
98
  async extract(zipFile, targetFolder) {
54
99
  let extractedEntriesCount = 0;
55
- this.isCanceled = false;
100
+ const token = new cancelable_1.CancellationToken();
101
+ this.token = token;
56
102
  if (this.isOverwrite()) {
57
103
  await exfs.rimraf(targetFolder);
58
104
  }
59
- if (this.isCanceled) {
105
+ if (token.isCancelled) {
60
106
  return Promise.reject(this.canceledError());
61
107
  }
62
108
  await exfs.ensureFolder(targetFolder);
63
- const zfile = await this.openZip(zipFile);
109
+ const realTargetFolder = await util.realpath(targetFolder);
110
+ const zfile = await this.openZip(zipFile, token);
64
111
  this.zipFile = zfile;
65
112
  zfile.readEntry();
66
113
  return new Promise((c, e) => {
114
+ let anyError = null;
67
115
  const total = zfile.entryCount;
68
116
  zfile.once("error", (err) => {
69
- e(this.wrapError(err));
117
+ this.closeZip();
118
+ e(this.wrapError(err, token.isCancelled));
70
119
  });
71
120
  zfile.once("close", () => {
72
- if (this.isCanceled) {
73
- e(this.canceledError());
121
+ this.zipFile = null;
122
+ if (anyError) {
123
+ e(this.wrapError(anyError, token.isCancelled));
74
124
  }
75
- // If the zip content is empty, it will not receive the `zfile.on("entry")` event.
76
- else if (total === 0) {
77
- c(void 0);
125
+ else {
126
+ if (token.isCancelled) {
127
+ e(this.canceledError());
128
+ }
129
+ // If the zip content is empty, it will not receive the `zfile.on("entry")` event.
130
+ else if (total === 0) {
131
+ c(void 0);
132
+ }
78
133
  }
79
134
  });
80
135
  // Because openZip is an asynchronous method, openZip may not be completed when calling cancel,
81
136
  // so we need to check if it has been canceled after the openZip method returns.
82
- if (this.isCanceled) {
137
+ if (token.isCancelled) {
83
138
  this.closeZip();
84
139
  return;
85
140
  }
141
+ const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile());
86
142
  const entryEvent = new EntryEvent(total);
87
143
  zfile.on("entry", async (entry) => {
88
144
  // use UTF-8 in all situations
@@ -94,19 +150,21 @@ class Unzip extends cancelable_1.Cancelable {
94
150
  // see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename
95
151
  const errorMessage = yauzl.validateFileName(fileName);
96
152
  if (errorMessage != null) {
97
- e(new Error(errorMessage));
153
+ anyError = new Error(errorMessage);
98
154
  this.closeZip();
155
+ e(anyError);
99
156
  return;
100
157
  }
101
158
  entryEvent.entryName = fileName;
102
159
  this.onEntryCallback(entryEvent);
160
+ entryContext.decodeEntryFileName = fileName;
103
161
  try {
104
162
  if (entryEvent.isPrevented) {
105
163
  entryEvent.reset();
106
164
  zfile.readEntry();
107
165
  }
108
166
  else {
109
- await this.handleEntry(zfile, entry, fileName, targetFolder);
167
+ await this.handleEntry(zfile, entry, entryContext, token);
110
168
  }
111
169
  extractedEntriesCount++;
112
170
  if (extractedEntriesCount === total) {
@@ -114,8 +172,9 @@ class Unzip extends cancelable_1.Cancelable {
114
172
  }
115
173
  }
116
174
  catch (error) {
117
- e(this.wrapError(error));
175
+ anyError = this.wrapError(error, token.isCancelled);
118
176
  this.closeZip();
177
+ e(anyError);
119
178
  }
120
179
  });
121
180
  });
@@ -125,9 +184,9 @@ class Unzip extends cancelable_1.Cancelable {
125
184
  * If the cancel method is called after the extract is complete, nothing will happen.
126
185
  */
127
186
  cancel() {
128
- super.cancel();
129
- if (this.cancelCallback) {
130
- this.cancelCallback(this.canceledError());
187
+ if (this.token) {
188
+ this.token.cancel();
189
+ this.token = null;
131
190
  }
132
191
  this.closeZip();
133
192
  }
@@ -137,7 +196,7 @@ class Unzip extends cancelable_1.Cancelable {
137
196
  this.zipFile = null;
138
197
  }
139
198
  }
140
- openZip(zipFile) {
199
+ openZip(zipFile, token) {
141
200
  return new Promise((c, e) => {
142
201
  yauzl.open(zipFile, {
143
202
  lazyEntries: true,
@@ -145,7 +204,7 @@ class Unzip extends cancelable_1.Cancelable {
145
204
  decodeStrings: false
146
205
  }, (err, zfile) => {
147
206
  if (err) {
148
- e(this.wrapError(err));
207
+ e(this.wrapError(err, token.isCancelled));
149
208
  }
150
209
  else {
151
210
  c(zfile);
@@ -153,24 +212,24 @@ class Unzip extends cancelable_1.Cancelable {
153
212
  });
154
213
  });
155
214
  }
156
- async handleEntry(zfile, entry, decodeEntryFileName, targetPath) {
157
- if (/\/$/.test(decodeEntryFileName)) {
215
+ async handleEntry(zfile, entry, entryContext, token) {
216
+ if (/\/$/.test(entryContext.decodeEntryFileName)) {
158
217
  // Directory file names end with '/'.
159
218
  // Note that entires for directories themselves are optional.
160
219
  // An entry's fileName implicitly requires its parent directories to exist.
161
- await exfs.ensureFolder(path.join(targetPath, decodeEntryFileName));
220
+ await exfs.ensureFolder(entryContext.getFilePath());
162
221
  zfile.readEntry();
163
222
  }
164
223
  else {
165
224
  // file entry
166
- await this.extractEntry(zfile, entry, decodeEntryFileName, targetPath);
225
+ await this.extractEntry(zfile, entry, entryContext, token);
167
226
  }
168
227
  }
169
- openZipFileStream(zfile, entry) {
228
+ openZipFileStream(zfile, entry, token) {
170
229
  return new Promise((c, e) => {
171
230
  zfile.openReadStream(entry, (err, readStream) => {
172
231
  if (err) {
173
- e(this.wrapError(err));
232
+ e(this.wrapError(err, token.isCancelled));
174
233
  }
175
234
  else {
176
235
  c(readStream);
@@ -178,32 +237,40 @@ class Unzip extends cancelable_1.Cancelable {
178
237
  });
179
238
  });
180
239
  }
181
- async extractEntry(zfile, entry, decodeEntryFileName, targetPath) {
182
- const filePath = path.join(targetPath, decodeEntryFileName);
183
- await exfs.ensureFolder(path.dirname(filePath));
184
- const readStream = await this.openZipFileStream(zfile, entry);
185
- readStream.on("end", () => {
186
- zfile.readEntry();
187
- });
188
- await this.writeEntryToFile(readStream, entry, filePath);
240
+ async extractEntry(zfile, entry, entryContext, token) {
241
+ const filePath = entryContext.getFilePath();
242
+ const fileDir = path.dirname(filePath);
243
+ await exfs.ensureFolder(fileDir);
244
+ const outside = await entryContext.isOutsideTargetFolder(fileDir);
245
+ if (outside) {
246
+ const error = new Error(`Refuse to write file outside "${entryContext.targetFolder}", file: "${filePath}"`);
247
+ error.name = "AFWRITE";
248
+ return Promise.reject(error);
249
+ }
250
+ const readStream = await this.openZipFileStream(zfile, entry, token);
251
+ await this.writeEntryToFile(readStream, entry, entryContext, token);
252
+ zfile.readEntry();
189
253
  }
190
- async writeEntryToFile(readStream, entry, filePath) {
254
+ async writeEntryToFile(readStream, entry, entryContext, token) {
191
255
  let fileStream;
192
- this.cancelCallback = (err) => {
193
- this.cancelCallback = undefined;
256
+ token.onCancelled(() => {
194
257
  if (fileStream) {
195
258
  readStream.unpipe(fileStream);
196
- fileStream.destroy(err);
259
+ fileStream.destroy(this.canceledError());
197
260
  }
198
- };
261
+ });
199
262
  return new Promise(async (c, e) => {
200
263
  try {
264
+ const filePath = entryContext.getFilePath();
201
265
  const mode = this.modeFromEntry(entry);
202
266
  // see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink
203
267
  const isSymlink = ((mode & 0o170000) === 0o120000);
204
268
  readStream.once("error", (err) => {
205
- e(this.wrapError(err));
269
+ e(this.wrapError(err, token.isCancelled));
206
270
  });
271
+ if (isSymlink) {
272
+ entryContext.symlinkFileNames.push(entryContext.decodeEntryFileName);
273
+ }
207
274
  if (isSymlink && !this.symlinkToFile()) {
208
275
  let linkContent = "";
209
276
  readStream.on("data", (chunk) => {
@@ -222,13 +289,13 @@ class Unzip extends cancelable_1.Cancelable {
222
289
  fileStream = fs_1.createWriteStream(filePath, { mode });
223
290
  fileStream.once("close", () => c());
224
291
  fileStream.once("error", (err) => {
225
- e(this.wrapError(err));
292
+ e(this.wrapError(err, token.isCancelled));
226
293
  });
227
294
  readStream.pipe(fileStream);
228
295
  }
229
296
  }
230
297
  catch (error) {
231
- e(this.wrapError(error));
298
+ e(this.wrapError(error, token.isCancelled));
232
299
  }
233
300
  });
234
301
  }
@@ -239,7 +306,28 @@ class Unzip extends cancelable_1.Cancelable {
239
306
  .reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
240
307
  }
241
308
  async createSymlink(linkContent, des) {
242
- await util.symlink(linkContent, des);
309
+ let linkType = "file";
310
+ if (process.platform === 'win32') {
311
+ if (/\/$/.test(linkContent)) {
312
+ linkType = "dir";
313
+ }
314
+ else {
315
+ let targetPath = linkContent;
316
+ if (!path.isAbsolute(linkContent)) {
317
+ targetPath = path.join(path.dirname(des), linkContent);
318
+ }
319
+ try {
320
+ const stat = await util.stat(targetPath);
321
+ if (stat.isDirectory()) {
322
+ linkType = "dir";
323
+ }
324
+ }
325
+ catch (error) {
326
+ // ignore
327
+ }
328
+ }
329
+ }
330
+ await util.symlink(linkContent, des, linkType);
243
331
  }
244
332
  isOverwrite() {
245
333
  if (this.options &&
@@ -268,4 +356,3 @@ class Unzip extends cancelable_1.Cancelable {
268
356
  }
269
357
  }
270
358
  exports.Unzip = Unzip;
271
- //# sourceMappingURL=unzip.js.map
package/lib/util.js CHANGED
@@ -1,89 +1,49 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readlink = exports.symlink = exports.rmdir = exports.access = exports.readdir = exports.chmod = exports.lstat = exports.stat = exports.realpath = exports.mkdir = exports.unlink = void 0;
3
4
  const fs = require("fs");
4
5
  const util = require("util");
5
- function asyncCall(fn, ...args) {
6
- return new Promise((c, e) => fn(...args, (err, result) => err ? e(err) : c(result)));
7
- }
8
6
  function unlink(path) {
9
- if (typeof util.promisify === "function") {
10
- return util.promisify(fs.unlink)(path);
11
- }
12
- else {
13
- return asyncCall(fs.unlink, path);
14
- }
7
+ return util.promisify(fs.unlink)(path);
15
8
  }
16
9
  exports.unlink = unlink;
17
10
  function mkdir(path, mode) {
18
- if (typeof util.promisify === "function") {
19
- return util.promisify(fs.mkdir)(path, mode);
20
- }
21
- else {
22
- return asyncCall(fs.mkdir, path, mode);
23
- }
11
+ return util.promisify(fs.mkdir)(path, mode);
24
12
  }
25
13
  exports.mkdir = mkdir;
14
+ function realpath(path) {
15
+ return util.promisify(fs.realpath)(path);
16
+ }
17
+ exports.realpath = realpath;
18
+ function stat(path) {
19
+ return util.promisify(fs.stat)(path);
20
+ }
21
+ exports.stat = stat;
26
22
  function lstat(path) {
27
- if (typeof util.promisify === "function") {
28
- return util.promisify(fs.lstat)(path);
29
- }
30
- else {
31
- return asyncCall(fs.lstat, path);
32
- }
23
+ return util.promisify(fs.lstat)(path);
33
24
  }
34
25
  exports.lstat = lstat;
35
26
  function chmod(path, mode) {
36
- if (typeof util.promisify === "function") {
37
- return util.promisify(fs.chmod)(path, mode);
38
- }
39
- else {
40
- return asyncCall(fs.chmod, path, mode);
41
- }
27
+ return util.promisify(fs.chmod)(path, mode);
42
28
  }
43
29
  exports.chmod = chmod;
44
30
  function readdir(path) {
45
- if (typeof util.promisify === "function") {
46
- return util.promisify(fs.readdir)(path);
47
- }
48
- else {
49
- return asyncCall(fs.readdir, path);
50
- }
31
+ return util.promisify(fs.readdir)(path);
51
32
  }
52
33
  exports.readdir = readdir;
53
34
  function access(path, mode) {
54
- if (typeof util.promisify === "function") {
55
- return util.promisify(fs.access)(path, mode);
56
- }
57
- else {
58
- return asyncCall(fs.access, path, mode);
59
- }
35
+ return util.promisify(fs.access)(path, mode);
60
36
  }
61
37
  exports.access = access;
62
38
  function rmdir(path) {
63
- if (typeof util.promisify === "function") {
64
- return util.promisify(fs.rmdir)(path);
65
- }
66
- else {
67
- return asyncCall(fs.rmdir, path);
68
- }
39
+ return util.promisify(fs.rmdir)(path);
69
40
  }
70
41
  exports.rmdir = rmdir;
71
- function symlink(target, path) {
72
- if (typeof util.promisify === "function") {
73
- return util.promisify(fs.symlink)(target, path);
74
- }
75
- else {
76
- return asyncCall(fs.symlink, target, path);
77
- }
42
+ function symlink(target, path, type) {
43
+ return util.promisify(fs.symlink)(target, path, type);
78
44
  }
79
45
  exports.symlink = symlink;
80
46
  function readlink(path) {
81
- if (typeof util.promisify === "function") {
82
- return util.promisify(fs.readlink)(path);
83
- }
84
- else {
85
- return asyncCall(fs.readlink, path);
86
- }
47
+ return util.promisify(fs.readlink)(path);
87
48
  }
88
49
  exports.readlink = readlink;
89
- //# sourceMappingURL=util.js.map
package/lib/zip.d.ts CHANGED
@@ -25,7 +25,7 @@ export declare class Zip extends Cancelable {
25
25
  private zipStream;
26
26
  private zipFiles;
27
27
  private zipFolders;
28
- private yazlErrorCallback?;
28
+ private token;
29
29
  /**
30
30
  * Adds a file from the file system at realPath into the zipfile as metadataPath.
31
31
  * @param file
@@ -50,8 +50,8 @@ export declare class Zip extends Cancelable {
50
50
  * If the cancel method is called after the archive is complete, nothing will happen.
51
51
  */
52
52
  cancel(): void;
53
- private initYazl;
54
- private addFileOrSymlink;
53
+ private addEntry;
54
+ private addFileStream;
55
55
  private addSymlink;
56
56
  private walkDir;
57
57
  private stopPipe;
package/lib/zip.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Zip = void 0;
3
4
  const yazl = require("yazl");
4
5
  const fs_1 = require("fs");
5
6
  const exfs = require("./fs");
@@ -45,7 +46,7 @@ class Zip extends cancelable_1.Cancelable {
45
46
  addFolder(folder, metadataPath) {
46
47
  this.zipFolders.push({
47
48
  path: folder,
48
- metadataPath: metadataPath
49
+ metadataPath
49
50
  });
50
51
  }
51
52
  /**
@@ -56,40 +57,25 @@ class Zip extends cancelable_1.Cancelable {
56
57
  if (!zipFile) {
57
58
  return Promise.reject(new Error("zipPath must not be empty"));
58
59
  }
59
- this.isCanceled = false;
60
+ const token = new cancelable_1.CancellationToken();
61
+ this.token = token;
60
62
  this.isPipe = false;
63
+ await exfs.ensureFolder(path.dirname(zipFile));
61
64
  // Re-instantiate yazl every time the archive method is called to ensure that files are not added repeatedly.
62
65
  // This will also make the Zip class reusable.
63
- this.initYazl();
66
+ this.yazlFile = new yazl.ZipFile();
64
67
  return new Promise(async (c, e) => {
65
- this.yazlErrorCallback = (err) => {
66
- this.yazlErrorCallback = undefined;
67
- e(err);
68
- };
68
+ this.yazlFile.once("error", (err) => {
69
+ e(this.wrapError(err, token.isCancelled));
70
+ });
69
71
  const zip = this.yazlFile;
70
- try {
71
- const files = this.zipFiles;
72
- for (let fi = 0; fi < files.length; fi++) {
73
- const file = files[fi];
74
- await this.addFileOrSymlink(zip, file.path, file.metadataPath);
75
- }
76
- if (this.zipFolders.length > 0) {
77
- await this.walkDir(this.zipFolders);
78
- }
79
- await exfs.ensureFolder(path.dirname(zipFile));
80
- }
81
- catch (error) {
82
- e(this.wrapError(error));
83
- return;
84
- }
85
- zip.end();
86
- if (!this.isCanceled) {
72
+ if (!token.isCancelled) {
87
73
  this.zipStream = fs_1.createWriteStream(zipFile);
88
74
  this.zipStream.once("error", (err) => {
89
- e(this.wrapError(err));
75
+ e(this.wrapError(err, token.isCancelled));
90
76
  });
91
77
  this.zipStream.once("close", () => {
92
- if (this.isCanceled) {
78
+ if (token.isCancelled) {
93
79
  e(this.canceledError());
94
80
  }
95
81
  else {
@@ -97,11 +83,26 @@ class Zip extends cancelable_1.Cancelable {
97
83
  }
98
84
  });
99
85
  zip.outputStream.once("error", (err) => {
100
- e(this.wrapError(err));
86
+ e(this.wrapError(err, token.isCancelled));
101
87
  });
102
88
  zip.outputStream.pipe(this.zipStream);
103
89
  this.isPipe = true;
104
90
  }
91
+ try {
92
+ const files = this.zipFiles;
93
+ for (const file of files) {
94
+ const entry = await exfs.getFileEntry(file.path);
95
+ await this.addEntry(zip, entry, file, token);
96
+ }
97
+ if (this.zipFolders.length > 0) {
98
+ await this.walkDir(this.zipFolders, token);
99
+ }
100
+ }
101
+ catch (error) {
102
+ e(this.wrapError(error, token.isCancelled));
103
+ return;
104
+ }
105
+ zip.end();
105
106
  });
106
107
  }
107
108
  /**
@@ -109,34 +110,58 @@ class Zip extends cancelable_1.Cancelable {
109
110
  * If the cancel method is called after the archive is complete, nothing will happen.
110
111
  */
111
112
  cancel() {
112
- super.cancel();
113
+ if (this.token) {
114
+ this.token.cancel();
115
+ this.token = null;
116
+ }
113
117
  this.stopPipe(this.canceledError());
114
118
  }
115
- initYazl() {
116
- this.yazlFile = new yazl.ZipFile();
117
- this.yazlFile.once("error", (err) => {
118
- this.stopPipe(this.wrapError(err));
119
- });
120
- }
121
- async addFileOrSymlink(zip, file, metadataPath) {
122
- if (this.followSymlink()) {
123
- zip.addFile(file, metadataPath);
119
+ async addEntry(zip, entry, file, token) {
120
+ if (entry.isSymbolicLink) {
121
+ if (this.followSymlink()) {
122
+ if (entry.type === "dir") {
123
+ const realPath = await util.realpath(file.path);
124
+ await this.walkDir([{ path: realPath, metadataPath: file.metadataPath }], token);
125
+ }
126
+ else {
127
+ zip.addFile(file.path, file.metadataPath);
128
+ }
129
+ }
130
+ else {
131
+ await this.addSymlink(zip, entry, file.metadataPath);
132
+ }
124
133
  }
125
134
  else {
126
- const stat = await util.lstat(file);
127
- if (stat.isSymbolicLink()) {
128
- await this.addSymlink(zip, {
129
- path: file,
130
- type: "file",
131
- mtime: stat.mtime,
132
- mode: stat.mode
133
- }, metadataPath);
135
+ if (entry.type === "dir") {
136
+ zip.addEmptyDirectory(file.metadataPath, {
137
+ mtime: entry.mtime,
138
+ mode: entry.mode
139
+ });
134
140
  }
135
141
  else {
136
- zip.addFile(file, metadataPath);
142
+ await this.addFileStream(zip, entry, file.metadataPath, token);
137
143
  }
138
144
  }
139
145
  }
146
+ addFileStream(zip, file, metadataPath, token) {
147
+ return new Promise((c, e) => {
148
+ const fileStream = fs_1.createReadStream(file.path);
149
+ fileStream.once("error", (err) => {
150
+ const wrappedError = this.wrapError(err, token.isCancelled);
151
+ this.stopPipe(wrappedError);
152
+ e(wrappedError);
153
+ });
154
+ fileStream.once("close", () => {
155
+ c();
156
+ });
157
+ // If the file attribute is known, add the entry using `addReadStream`,
158
+ // this can reduce the number of calls to the `fs.stat` method.
159
+ zip.addReadStream(fileStream, metadataPath, {
160
+ mode: file.mode,
161
+ mtime: file.mtime
162
+ });
163
+ });
164
+ }
140
165
  async addSymlink(zip, file, metadataPath) {
141
166
  const linkTarget = await util.readlink(file.path);
142
167
  zip.addBuffer(Buffer.from(linkTarget), metadataPath, {
@@ -144,33 +169,20 @@ class Zip extends cancelable_1.Cancelable {
144
169
  mode: file.mode
145
170
  });
146
171
  }
147
- async walkDir(folders) {
148
- for (let fi = 0; fi < folders.length; fi++) {
149
- if (this.isCanceled) {
172
+ async walkDir(folders, token) {
173
+ for (const folder of folders) {
174
+ if (token.isCancelled) {
150
175
  return;
151
176
  }
152
- const folder = folders[fi];
153
177
  const entries = await exfs.readdirp(folder.path);
154
178
  if (entries.length > 0) {
155
- for (let ei = 0; ei < entries.length; ei++) {
156
- const entry = entries[ei];
157
- if (this.isCanceled) {
179
+ for (const entry of entries) {
180
+ if (token.isCancelled) {
158
181
  return;
159
182
  }
160
183
  const relativePath = path.relative(folder.path, entry.path);
161
184
  const metadataPath = folder.metadataPath ? path.join(folder.metadataPath, relativePath) : relativePath;
162
- if (entry.type === "dir") {
163
- this.yazlFile.addEmptyDirectory(metadataPath, {
164
- mtime: entry.mtime,
165
- mode: entry.mode
166
- });
167
- }
168
- else if (entry.type === "symlink" && !this.followSymlink()) {
169
- await this.addSymlink(this.yazlFile, entry, metadataPath);
170
- }
171
- else {
172
- this.yazlFile.addFile(entry.path, metadataPath);
173
- }
185
+ await this.addEntry(this.yazlFile, entry, { path: entry.path, metadataPath }, token);
174
186
  }
175
187
  }
176
188
  else {
@@ -183,9 +195,6 @@ class Zip extends cancelable_1.Cancelable {
183
195
  }
184
196
  }
185
197
  stopPipe(err) {
186
- if (this.yazlErrorCallback) {
187
- this.yazlErrorCallback(err);
188
- }
189
198
  if (this.isPipe) {
190
199
  this.yazlFile.outputStream.unpipe(this.zipStream);
191
200
  this.zipStream.destroy(err);
@@ -202,4 +211,3 @@ class Zip extends cancelable_1.Cancelable {
202
211
  }
203
212
  }
204
213
  exports.Zip = Zip;
205
- //# sourceMappingURL=zip.js.map
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "zip-lib",
3
- "version": "0.5.0",
3
+ "version": "0.7.2",
4
4
  "description": "zip and unzip library for node",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
7
7
  "compile": "rimraf ./lib && tsc -p ./src/tsconfig.json",
8
+ "release": "rimraf ./lib && tsc -p ./src/tsconfig.release.json",
8
9
  "compile-test": "rimraf ./test/out && tsc -p ./test/src/tsconfig.json",
9
- "test": "node ./test/src/before.js && mocha ./test/out --timeout 5000"
10
+ "test": "node ./test/src/before.js && mocha ./test/out --timeout 10000",
11
+ "pack": "npm run release && npm pack"
10
12
  },
11
13
  "repository": {
12
14
  "type": "git",
@@ -28,12 +30,12 @@
28
30
  "yazl": "^2.5.1"
29
31
  },
30
32
  "devDependencies": {
31
- "@types/mocha": "^5.2.7",
32
- "@types/node": "^8.10.56",
33
+ "@types/mocha": "^8.2.0",
34
+ "@types/node": "^8.10.66",
33
35
  "@types/yauzl": "^2.9.1",
34
36
  "@types/yazl": "^2.4.2",
35
- "mocha": "^6.2.2",
36
- "rimraf": "^3.0.0",
37
- "typescript": "^3.6.4"
37
+ "mocha": "^8.2.1",
38
+ "rimraf": "^3.0.2",
39
+ "typescript": "^4.1.3"
38
40
  }
39
41
  }