zip-lib 0.7.2 → 1.0.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/lib/cancelable.d.ts +38 -38
- package/lib/cancelable.js +65 -65
- package/lib/fs.js +175 -175
- package/lib/index.d.ts +25 -25
- package/lib/index.js +56 -52
- package/lib/unzip.d.ts +78 -78
- package/lib/unzip.js +358 -358
- package/lib/util.js +49 -49
- package/lib/zip.d.ts +59 -59
- package/lib/zip.js +213 -213
- package/package.json +12 -9
package/lib/unzip.js
CHANGED
|
@@ -1,358 +1,358 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Unzip = void 0;
|
|
4
|
-
const yauzl = require("yauzl");
|
|
5
|
-
const exfs = require("./fs");
|
|
6
|
-
const fs_1 = require("fs");
|
|
7
|
-
const path = require("path");
|
|
8
|
-
const util = require("./util");
|
|
9
|
-
const cancelable_1 = require("./cancelable");
|
|
10
|
-
class EntryEvent {
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
13
|
-
*/
|
|
14
|
-
constructor(_entryCount) {
|
|
15
|
-
this._entryCount = _entryCount;
|
|
16
|
-
this._isPrevented = false;
|
|
17
|
-
}
|
|
18
|
-
get entryName() {
|
|
19
|
-
return this._entryName;
|
|
20
|
-
}
|
|
21
|
-
set entryName(name) {
|
|
22
|
-
this._entryName = name;
|
|
23
|
-
}
|
|
24
|
-
get entryCount() {
|
|
25
|
-
return this._entryCount;
|
|
26
|
-
}
|
|
27
|
-
get isPrevented() {
|
|
28
|
-
return this._isPrevented;
|
|
29
|
-
}
|
|
30
|
-
preventDefault() {
|
|
31
|
-
this._isPrevented = true;
|
|
32
|
-
}
|
|
33
|
-
reset() {
|
|
34
|
-
this._isPrevented = false;
|
|
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
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Extract the zip file.
|
|
83
|
-
*/
|
|
84
|
-
class Unzip extends cancelable_1.Cancelable {
|
|
85
|
-
/**
|
|
86
|
-
*
|
|
87
|
-
*/
|
|
88
|
-
constructor(options) {
|
|
89
|
-
super();
|
|
90
|
-
this.options = options;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* Extract the zip file to the specified location.
|
|
94
|
-
* @param zipFile
|
|
95
|
-
* @param targetFolder
|
|
96
|
-
* @param options
|
|
97
|
-
*/
|
|
98
|
-
async extract(zipFile, targetFolder) {
|
|
99
|
-
let extractedEntriesCount = 0;
|
|
100
|
-
const token = new cancelable_1.CancellationToken();
|
|
101
|
-
this.token = token;
|
|
102
|
-
if (this.isOverwrite()) {
|
|
103
|
-
await exfs.rimraf(targetFolder);
|
|
104
|
-
}
|
|
105
|
-
if (token.isCancelled) {
|
|
106
|
-
return Promise.reject(this.canceledError());
|
|
107
|
-
}
|
|
108
|
-
await exfs.ensureFolder(targetFolder);
|
|
109
|
-
const realTargetFolder = await util.realpath(targetFolder);
|
|
110
|
-
const zfile = await this.openZip(zipFile, token);
|
|
111
|
-
this.zipFile = zfile;
|
|
112
|
-
zfile.readEntry();
|
|
113
|
-
return new Promise((c, e) => {
|
|
114
|
-
let anyError = null;
|
|
115
|
-
const total = zfile.entryCount;
|
|
116
|
-
zfile.once("error", (err) => {
|
|
117
|
-
this.closeZip();
|
|
118
|
-
e(this.wrapError(err, token.isCancelled));
|
|
119
|
-
});
|
|
120
|
-
zfile.once("close", () => {
|
|
121
|
-
this.zipFile = null;
|
|
122
|
-
if (anyError) {
|
|
123
|
-
e(this.wrapError(anyError, token.isCancelled));
|
|
124
|
-
}
|
|
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
|
-
}
|
|
133
|
-
}
|
|
134
|
-
});
|
|
135
|
-
// Because openZip is an asynchronous method, openZip may not be completed when calling cancel,
|
|
136
|
-
// so we need to check if it has been canceled after the openZip method returns.
|
|
137
|
-
if (token.isCancelled) {
|
|
138
|
-
this.closeZip();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile());
|
|
142
|
-
const entryEvent = new EntryEvent(total);
|
|
143
|
-
zfile.on("entry", async (entry) => {
|
|
144
|
-
// use UTF-8 in all situations
|
|
145
|
-
// see https://github.com/thejoshwolfe/yauzl/issues/84
|
|
146
|
-
const rawName = entry.fileName.toString("utf8");
|
|
147
|
-
// allow backslash
|
|
148
|
-
const fileName = rawName.replace(/\\/g, "/");
|
|
149
|
-
// Because `decodeStrings` is `false`, we need to manually verify the entryname
|
|
150
|
-
// see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename
|
|
151
|
-
const errorMessage = yauzl.validateFileName(fileName);
|
|
152
|
-
if (errorMessage != null) {
|
|
153
|
-
anyError = new Error(errorMessage);
|
|
154
|
-
this.closeZip();
|
|
155
|
-
e(anyError);
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
entryEvent.entryName = fileName;
|
|
159
|
-
this.onEntryCallback(entryEvent);
|
|
160
|
-
entryContext.decodeEntryFileName = fileName;
|
|
161
|
-
try {
|
|
162
|
-
if (entryEvent.isPrevented) {
|
|
163
|
-
entryEvent.reset();
|
|
164
|
-
zfile.readEntry();
|
|
165
|
-
}
|
|
166
|
-
else {
|
|
167
|
-
await this.handleEntry(zfile, entry, entryContext, token);
|
|
168
|
-
}
|
|
169
|
-
extractedEntriesCount++;
|
|
170
|
-
if (extractedEntriesCount === total) {
|
|
171
|
-
c();
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
anyError = this.wrapError(error, token.isCancelled);
|
|
176
|
-
this.closeZip();
|
|
177
|
-
e(anyError);
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
/**
|
|
183
|
-
* Cancel decompression.
|
|
184
|
-
* If the cancel method is called after the extract is complete, nothing will happen.
|
|
185
|
-
*/
|
|
186
|
-
cancel() {
|
|
187
|
-
if (this.token) {
|
|
188
|
-
this.token.cancel();
|
|
189
|
-
this.token = null;
|
|
190
|
-
}
|
|
191
|
-
this.closeZip();
|
|
192
|
-
}
|
|
193
|
-
closeZip() {
|
|
194
|
-
if (this.zipFile) {
|
|
195
|
-
this.zipFile.close();
|
|
196
|
-
this.zipFile = null;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
openZip(zipFile, token) {
|
|
200
|
-
return new Promise((c, e) => {
|
|
201
|
-
yauzl.open(zipFile, {
|
|
202
|
-
lazyEntries: true,
|
|
203
|
-
// see https://github.com/thejoshwolfe/yauzl/issues/84
|
|
204
|
-
decodeStrings: false
|
|
205
|
-
}, (err, zfile) => {
|
|
206
|
-
if (err) {
|
|
207
|
-
e(this.wrapError(err, token.isCancelled));
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
c(zfile);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
async handleEntry(zfile, entry, entryContext, token) {
|
|
216
|
-
if (/\/$/.test(entryContext.decodeEntryFileName)) {
|
|
217
|
-
// Directory file names end with '/'.
|
|
218
|
-
// Note that entires for directories themselves are optional.
|
|
219
|
-
// An entry's fileName implicitly requires its parent directories to exist.
|
|
220
|
-
await exfs.ensureFolder(entryContext.getFilePath());
|
|
221
|
-
zfile.readEntry();
|
|
222
|
-
}
|
|
223
|
-
else {
|
|
224
|
-
// file entry
|
|
225
|
-
await this.extractEntry(zfile, entry, entryContext, token);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
openZipFileStream(zfile, entry, token) {
|
|
229
|
-
return new Promise((c, e) => {
|
|
230
|
-
zfile.openReadStream(entry, (err, readStream) => {
|
|
231
|
-
if (err) {
|
|
232
|
-
e(this.wrapError(err, token.isCancelled));
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
c(readStream);
|
|
236
|
-
}
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
}
|
|
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();
|
|
253
|
-
}
|
|
254
|
-
async writeEntryToFile(readStream, entry, entryContext, token) {
|
|
255
|
-
let fileStream;
|
|
256
|
-
token.onCancelled(() => {
|
|
257
|
-
if (fileStream) {
|
|
258
|
-
readStream.unpipe(fileStream);
|
|
259
|
-
fileStream.destroy(this.canceledError());
|
|
260
|
-
}
|
|
261
|
-
});
|
|
262
|
-
return new Promise(async (c, e) => {
|
|
263
|
-
try {
|
|
264
|
-
const filePath = entryContext.getFilePath();
|
|
265
|
-
const mode = this.modeFromEntry(entry);
|
|
266
|
-
// see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink
|
|
267
|
-
const isSymlink = ((mode & 0o170000) === 0o120000);
|
|
268
|
-
readStream.once("error", (err) => {
|
|
269
|
-
e(this.wrapError(err, token.isCancelled));
|
|
270
|
-
});
|
|
271
|
-
if (isSymlink) {
|
|
272
|
-
entryContext.symlinkFileNames.push(entryContext.decodeEntryFileName);
|
|
273
|
-
}
|
|
274
|
-
if (isSymlink && !this.symlinkToFile()) {
|
|
275
|
-
let linkContent = "";
|
|
276
|
-
readStream.on("data", (chunk) => {
|
|
277
|
-
if (chunk instanceof String) {
|
|
278
|
-
linkContent += chunk;
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
linkContent += chunk.toString();
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
readStream.once("end", () => {
|
|
285
|
-
this.createSymlink(linkContent, filePath).then(c, e);
|
|
286
|
-
});
|
|
287
|
-
}
|
|
288
|
-
else {
|
|
289
|
-
fileStream = fs_1.createWriteStream(filePath, { mode });
|
|
290
|
-
fileStream.once("close", () => c());
|
|
291
|
-
fileStream.once("error", (err) => {
|
|
292
|
-
e(this.wrapError(err, token.isCancelled));
|
|
293
|
-
});
|
|
294
|
-
readStream.pipe(fileStream);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
catch (error) {
|
|
298
|
-
e(this.wrapError(error, token.isCancelled));
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
modeFromEntry(entry) {
|
|
303
|
-
const attr = entry.externalFileAttributes >> 16 || 33188;
|
|
304
|
-
return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
|
|
305
|
-
.map(mask => attr & mask)
|
|
306
|
-
.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
|
|
307
|
-
}
|
|
308
|
-
async createSymlink(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);
|
|
331
|
-
}
|
|
332
|
-
isOverwrite() {
|
|
333
|
-
if (this.options &&
|
|
334
|
-
this.options.overwrite) {
|
|
335
|
-
return true;
|
|
336
|
-
}
|
|
337
|
-
return false;
|
|
338
|
-
}
|
|
339
|
-
onEntryCallback(event) {
|
|
340
|
-
if (this.options && this.options.onEntry) {
|
|
341
|
-
this.options.onEntry(event);
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
symlinkToFile() {
|
|
345
|
-
let symlinkToFile = false;
|
|
346
|
-
if (process.platform === "win32") {
|
|
347
|
-
if (this.options &&
|
|
348
|
-
this.options.symlinkAsFileOnWindows === false) {
|
|
349
|
-
symlinkToFile = false;
|
|
350
|
-
}
|
|
351
|
-
else {
|
|
352
|
-
symlinkToFile = true;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
return symlinkToFile;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
exports.Unzip = Unzip;
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Unzip = void 0;
|
|
4
|
+
const yauzl = require("yauzl");
|
|
5
|
+
const exfs = require("./fs");
|
|
6
|
+
const fs_1 = require("fs");
|
|
7
|
+
const path = require("path");
|
|
8
|
+
const util = require("./util");
|
|
9
|
+
const cancelable_1 = require("./cancelable");
|
|
10
|
+
class EntryEvent {
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
constructor(_entryCount) {
|
|
15
|
+
this._entryCount = _entryCount;
|
|
16
|
+
this._isPrevented = false;
|
|
17
|
+
}
|
|
18
|
+
get entryName() {
|
|
19
|
+
return this._entryName;
|
|
20
|
+
}
|
|
21
|
+
set entryName(name) {
|
|
22
|
+
this._entryName = name;
|
|
23
|
+
}
|
|
24
|
+
get entryCount() {
|
|
25
|
+
return this._entryCount;
|
|
26
|
+
}
|
|
27
|
+
get isPrevented() {
|
|
28
|
+
return this._isPrevented;
|
|
29
|
+
}
|
|
30
|
+
preventDefault() {
|
|
31
|
+
this._isPrevented = true;
|
|
32
|
+
}
|
|
33
|
+
reset() {
|
|
34
|
+
this._isPrevented = false;
|
|
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
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Extract the zip file.
|
|
83
|
+
*/
|
|
84
|
+
class Unzip extends cancelable_1.Cancelable {
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
*/
|
|
88
|
+
constructor(options) {
|
|
89
|
+
super();
|
|
90
|
+
this.options = options;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Extract the zip file to the specified location.
|
|
94
|
+
* @param zipFile
|
|
95
|
+
* @param targetFolder
|
|
96
|
+
* @param options
|
|
97
|
+
*/
|
|
98
|
+
async extract(zipFile, targetFolder) {
|
|
99
|
+
let extractedEntriesCount = 0;
|
|
100
|
+
const token = new cancelable_1.CancellationToken();
|
|
101
|
+
this.token = token;
|
|
102
|
+
if (this.isOverwrite()) {
|
|
103
|
+
await exfs.rimraf(targetFolder);
|
|
104
|
+
}
|
|
105
|
+
if (token.isCancelled) {
|
|
106
|
+
return Promise.reject(this.canceledError());
|
|
107
|
+
}
|
|
108
|
+
await exfs.ensureFolder(targetFolder);
|
|
109
|
+
const realTargetFolder = await util.realpath(targetFolder);
|
|
110
|
+
const zfile = await this.openZip(zipFile, token);
|
|
111
|
+
this.zipFile = zfile;
|
|
112
|
+
zfile.readEntry();
|
|
113
|
+
return new Promise((c, e) => {
|
|
114
|
+
let anyError = null;
|
|
115
|
+
const total = zfile.entryCount;
|
|
116
|
+
zfile.once("error", (err) => {
|
|
117
|
+
this.closeZip();
|
|
118
|
+
e(this.wrapError(err, token.isCancelled));
|
|
119
|
+
});
|
|
120
|
+
zfile.once("close", () => {
|
|
121
|
+
this.zipFile = null;
|
|
122
|
+
if (anyError) {
|
|
123
|
+
e(this.wrapError(anyError, token.isCancelled));
|
|
124
|
+
}
|
|
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
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
// Because openZip is an asynchronous method, openZip may not be completed when calling cancel,
|
|
136
|
+
// so we need to check if it has been canceled after the openZip method returns.
|
|
137
|
+
if (token.isCancelled) {
|
|
138
|
+
this.closeZip();
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile());
|
|
142
|
+
const entryEvent = new EntryEvent(total);
|
|
143
|
+
zfile.on("entry", async (entry) => {
|
|
144
|
+
// use UTF-8 in all situations
|
|
145
|
+
// see https://github.com/thejoshwolfe/yauzl/issues/84
|
|
146
|
+
const rawName = entry.fileName.toString("utf8");
|
|
147
|
+
// allow backslash
|
|
148
|
+
const fileName = rawName.replace(/\\/g, "/");
|
|
149
|
+
// Because `decodeStrings` is `false`, we need to manually verify the entryname
|
|
150
|
+
// see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename
|
|
151
|
+
const errorMessage = yauzl.validateFileName(fileName);
|
|
152
|
+
if (errorMessage != null) {
|
|
153
|
+
anyError = new Error(errorMessage);
|
|
154
|
+
this.closeZip();
|
|
155
|
+
e(anyError);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
entryEvent.entryName = fileName;
|
|
159
|
+
this.onEntryCallback(entryEvent);
|
|
160
|
+
entryContext.decodeEntryFileName = fileName;
|
|
161
|
+
try {
|
|
162
|
+
if (entryEvent.isPrevented) {
|
|
163
|
+
entryEvent.reset();
|
|
164
|
+
zfile.readEntry();
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
await this.handleEntry(zfile, entry, entryContext, token);
|
|
168
|
+
}
|
|
169
|
+
extractedEntriesCount++;
|
|
170
|
+
if (extractedEntriesCount === total) {
|
|
171
|
+
c();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
anyError = this.wrapError(error, token.isCancelled);
|
|
176
|
+
this.closeZip();
|
|
177
|
+
e(anyError);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Cancel decompression.
|
|
184
|
+
* If the cancel method is called after the extract is complete, nothing will happen.
|
|
185
|
+
*/
|
|
186
|
+
cancel() {
|
|
187
|
+
if (this.token) {
|
|
188
|
+
this.token.cancel();
|
|
189
|
+
this.token = null;
|
|
190
|
+
}
|
|
191
|
+
this.closeZip();
|
|
192
|
+
}
|
|
193
|
+
closeZip() {
|
|
194
|
+
if (this.zipFile) {
|
|
195
|
+
this.zipFile.close();
|
|
196
|
+
this.zipFile = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
openZip(zipFile, token) {
|
|
200
|
+
return new Promise((c, e) => {
|
|
201
|
+
yauzl.open(zipFile, {
|
|
202
|
+
lazyEntries: true,
|
|
203
|
+
// see https://github.com/thejoshwolfe/yauzl/issues/84
|
|
204
|
+
decodeStrings: false
|
|
205
|
+
}, (err, zfile) => {
|
|
206
|
+
if (err) {
|
|
207
|
+
e(this.wrapError(err, token.isCancelled));
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
c(zfile);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
async handleEntry(zfile, entry, entryContext, token) {
|
|
216
|
+
if (/\/$/.test(entryContext.decodeEntryFileName)) {
|
|
217
|
+
// Directory file names end with '/'.
|
|
218
|
+
// Note that entires for directories themselves are optional.
|
|
219
|
+
// An entry's fileName implicitly requires its parent directories to exist.
|
|
220
|
+
await exfs.ensureFolder(entryContext.getFilePath());
|
|
221
|
+
zfile.readEntry();
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
// file entry
|
|
225
|
+
await this.extractEntry(zfile, entry, entryContext, token);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
openZipFileStream(zfile, entry, token) {
|
|
229
|
+
return new Promise((c, e) => {
|
|
230
|
+
zfile.openReadStream(entry, (err, readStream) => {
|
|
231
|
+
if (err) {
|
|
232
|
+
e(this.wrapError(err, token.isCancelled));
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
c(readStream);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
}
|
|
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();
|
|
253
|
+
}
|
|
254
|
+
async writeEntryToFile(readStream, entry, entryContext, token) {
|
|
255
|
+
let fileStream;
|
|
256
|
+
token.onCancelled(() => {
|
|
257
|
+
if (fileStream) {
|
|
258
|
+
readStream.unpipe(fileStream);
|
|
259
|
+
fileStream.destroy(this.canceledError());
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
return new Promise(async (c, e) => {
|
|
263
|
+
try {
|
|
264
|
+
const filePath = entryContext.getFilePath();
|
|
265
|
+
const mode = this.modeFromEntry(entry);
|
|
266
|
+
// see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink
|
|
267
|
+
const isSymlink = ((mode & 0o170000) === 0o120000);
|
|
268
|
+
readStream.once("error", (err) => {
|
|
269
|
+
e(this.wrapError(err, token.isCancelled));
|
|
270
|
+
});
|
|
271
|
+
if (isSymlink) {
|
|
272
|
+
entryContext.symlinkFileNames.push(entryContext.decodeEntryFileName);
|
|
273
|
+
}
|
|
274
|
+
if (isSymlink && !this.symlinkToFile()) {
|
|
275
|
+
let linkContent = "";
|
|
276
|
+
readStream.on("data", (chunk) => {
|
|
277
|
+
if (chunk instanceof String) {
|
|
278
|
+
linkContent += chunk;
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
linkContent += chunk.toString();
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
readStream.once("end", () => {
|
|
285
|
+
this.createSymlink(linkContent, filePath).then(c, e);
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
fileStream = (0, fs_1.createWriteStream)(filePath, { mode });
|
|
290
|
+
fileStream.once("close", () => c());
|
|
291
|
+
fileStream.once("error", (err) => {
|
|
292
|
+
e(this.wrapError(err, token.isCancelled));
|
|
293
|
+
});
|
|
294
|
+
readStream.pipe(fileStream);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
e(this.wrapError(error, token.isCancelled));
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
modeFromEntry(entry) {
|
|
303
|
+
const attr = entry.externalFileAttributes >> 16 || 33188;
|
|
304
|
+
return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
|
|
305
|
+
.map(mask => attr & mask)
|
|
306
|
+
.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
|
|
307
|
+
}
|
|
308
|
+
async createSymlink(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);
|
|
331
|
+
}
|
|
332
|
+
isOverwrite() {
|
|
333
|
+
if (this.options &&
|
|
334
|
+
this.options.overwrite) {
|
|
335
|
+
return true;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
onEntryCallback(event) {
|
|
340
|
+
if (this.options && this.options.onEntry) {
|
|
341
|
+
this.options.onEntry(event);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
symlinkToFile() {
|
|
345
|
+
let symlinkToFile = false;
|
|
346
|
+
if (process.platform === "win32") {
|
|
347
|
+
if (this.options &&
|
|
348
|
+
this.options.symlinkAsFileOnWindows === false) {
|
|
349
|
+
symlinkToFile = false;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
symlinkToFile = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return symlinkToFile;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
exports.Unzip = Unzip;
|