@appium/support 7.0.4 → 7.0.6
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/build/lib/console.d.ts +42 -88
- package/build/lib/console.d.ts.map +1 -1
- package/build/lib/console.js +25 -85
- package/build/lib/console.js.map +1 -1
- package/build/lib/doctor.d.ts +6 -18
- package/build/lib/doctor.d.ts.map +1 -1
- package/build/lib/doctor.js +0 -15
- package/build/lib/doctor.js.map +1 -1
- package/build/lib/env.d.ts +14 -20
- package/build/lib/env.d.ts.map +1 -1
- package/build/lib/env.js +24 -61
- package/build/lib/env.js.map +1 -1
- package/build/lib/fs.d.ts +109 -148
- package/build/lib/fs.d.ts.map +1 -1
- package/build/lib/fs.js +130 -230
- package/build/lib/fs.js.map +1 -1
- package/build/lib/image-util.d.ts +7 -6
- package/build/lib/image-util.d.ts.map +1 -1
- package/build/lib/image-util.js +9 -6
- package/build/lib/image-util.js.map +1 -1
- package/build/lib/index.d.ts +19 -17
- package/build/lib/index.d.ts.map +1 -1
- package/build/lib/logger.d.ts +1 -1
- package/build/lib/logger.d.ts.map +1 -1
- package/build/lib/logger.js +1 -1
- package/build/lib/logger.js.map +1 -1
- package/build/lib/logging.d.ts +7 -15
- package/build/lib/logging.d.ts.map +1 -1
- package/build/lib/logging.js +36 -62
- package/build/lib/logging.js.map +1 -1
- package/build/lib/mjpeg.d.ts +19 -56
- package/build/lib/mjpeg.d.ts.map +1 -1
- package/build/lib/mjpeg.js +55 -78
- package/build/lib/mjpeg.js.map +1 -1
- package/build/lib/mkdirp.d.ts +4 -1
- package/build/lib/mkdirp.d.ts.map +1 -1
- package/build/lib/mkdirp.js +1 -2
- package/build/lib/mkdirp.js.map +1 -1
- package/build/lib/net.d.ts +52 -90
- package/build/lib/net.d.ts.map +1 -1
- package/build/lib/net.js +104 -193
- package/build/lib/net.js.map +1 -1
- package/build/lib/node.d.ts +16 -17
- package/build/lib/node.d.ts.map +1 -1
- package/build/lib/node.js +115 -120
- package/build/lib/node.js.map +1 -1
- package/build/lib/npm.d.ts +65 -86
- package/build/lib/npm.d.ts.map +1 -1
- package/build/lib/npm.js +64 -122
- package/build/lib/npm.js.map +1 -1
- package/build/lib/plist.d.ts +36 -29
- package/build/lib/plist.d.ts.map +1 -1
- package/build/lib/plist.js +62 -59
- package/build/lib/plist.js.map +1 -1
- package/build/lib/process.d.ts +19 -2
- package/build/lib/process.d.ts.map +1 -1
- package/build/lib/process.js +24 -7
- package/build/lib/process.js.map +1 -1
- package/build/lib/system.d.ts +41 -6
- package/build/lib/system.d.ts.map +1 -1
- package/build/lib/system.js +49 -14
- package/build/lib/system.js.map +1 -1
- package/build/lib/tempdir.d.ts +26 -49
- package/build/lib/tempdir.d.ts.map +1 -1
- package/build/lib/tempdir.js +46 -78
- package/build/lib/tempdir.js.map +1 -1
- package/build/lib/timing.d.ts +28 -22
- package/build/lib/timing.d.ts.map +1 -1
- package/build/lib/timing.js +16 -17
- package/build/lib/timing.js.map +1 -1
- package/build/lib/util.d.ts +164 -181
- package/build/lib/util.d.ts.map +1 -1
- package/build/lib/util.js +198 -253
- package/build/lib/util.js.map +1 -1
- package/build/lib/zip.d.ts +81 -139
- package/build/lib/zip.d.ts.map +1 -1
- package/build/lib/zip.js +235 -283
- package/build/lib/zip.js.map +1 -1
- package/lib/console.ts +139 -0
- package/lib/{doctor.js → doctor.ts} +6 -20
- package/lib/{env.js → env.ts} +34 -62
- package/lib/fs.ts +453 -0
- package/lib/image-util.ts +40 -0
- package/lib/index.ts +1 -0
- package/lib/{logger.js → logger.ts} +1 -1
- package/lib/logging.ts +157 -0
- package/lib/mjpeg.ts +186 -0
- package/lib/{mkdirp.js → mkdirp.ts} +2 -2
- package/lib/net.ts +305 -0
- package/lib/{node.js → node.ts} +136 -135
- package/lib/npm.ts +291 -0
- package/lib/plist.ts +187 -0
- package/lib/process.ts +62 -0
- package/lib/system.ts +95 -0
- package/lib/tempdir.ts +115 -0
- package/lib/{timing.js → timing.ts} +28 -33
- package/lib/util.ts +561 -0
- package/lib/{zip.js → zip.ts} +344 -299
- package/package.json +24 -26
- package/tsconfig.json +3 -5
- package/index.js +0 -1
- package/lib/console.js +0 -173
- package/lib/fs.js +0 -496
- package/lib/image-util.js +0 -32
- package/lib/logging.js +0 -145
- package/lib/mjpeg.js +0 -207
- package/lib/net.js +0 -336
- package/lib/npm.js +0 -310
- package/lib/plist.js +0 -182
- package/lib/process.js +0 -46
- package/lib/system.js +0 -48
- package/lib/tempdir.js +0 -131
- package/lib/util.js +0 -585
package/lib/{zip.js → zip.ts}
RENAMED
|
@@ -1,199 +1,60 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
|
-
import
|
|
2
|
+
import {promisify} from 'node:util';
|
|
3
3
|
import * as yauzl from 'yauzl';
|
|
4
4
|
import archiver from 'archiver';
|
|
5
|
-
import {createWriteStream} from 'fs';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import stream from 'stream';
|
|
5
|
+
import {createWriteStream} from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import stream from 'node:stream';
|
|
8
|
+
import {pipeline} from 'node:stream/promises';
|
|
8
9
|
import {fs} from './fs';
|
|
9
10
|
import {isWindows} from './system';
|
|
10
11
|
import {Base64Encode} from 'base64-stream';
|
|
11
|
-
import {toReadableSizeString, GiB} from './util';
|
|
12
|
+
import {isSubPath, toReadableSizeString, GiB} from './util';
|
|
12
13
|
import {Timer} from './timing';
|
|
13
14
|
import log from './logger';
|
|
14
15
|
import getStream from 'get-stream';
|
|
15
16
|
import {exec} from 'teen_process';
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* @type {(source: NodeJS.ReadableStream, destination: NodeJS.WritableStream) => Promise<NodeJS.WritableStream>}
|
|
23
|
-
*/
|
|
24
|
-
const pipeline = B.promisify(stream.pipeline);
|
|
25
|
-
const ZIP_MAGIC = 'PK';
|
|
26
|
-
const IFMT = 61440;
|
|
27
|
-
const IFDIR = 16384;
|
|
28
|
-
const IFLNK = 40960;
|
|
29
|
-
|
|
30
|
-
// This class is mostly copied from https://github.com/maxogden/extract-zip/blob/master/index.js
|
|
31
|
-
class ZipExtractor {
|
|
32
|
-
/** @type {yauzl.ZipFile} */
|
|
33
|
-
zipfile;
|
|
34
|
-
|
|
35
|
-
constructor(sourcePath, opts = {}) {
|
|
36
|
-
this.zipPath = sourcePath;
|
|
37
|
-
this.opts = opts;
|
|
38
|
-
this.canceled = false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
extractFileName(entry) {
|
|
42
|
-
return _.isBuffer(entry.fileName)
|
|
43
|
-
? entry.fileName.toString(this.opts.fileNamesEncoding)
|
|
44
|
-
: entry.fileName;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async extract() {
|
|
48
|
-
const {dir, fileNamesEncoding} = this.opts;
|
|
49
|
-
this.zipfile = await openZip(this.zipPath, {
|
|
50
|
-
lazyEntries: true,
|
|
51
|
-
// https://github.com/thejoshwolfe/yauzl/commit/cc7455ac789ba84973184e5ebde0581cdc4c3b39#diff-04c6e90faac2675aa89e2176d2eec7d8R95
|
|
52
|
-
decodeStrings: !fileNamesEncoding,
|
|
53
|
-
});
|
|
54
|
-
this.canceled = false;
|
|
18
|
+
const openZip = promisify(yauzl.open) as (
|
|
19
|
+
zipPath: string,
|
|
20
|
+
options?: yauzl.Options
|
|
21
|
+
) => Promise<yauzl.ZipFile>;
|
|
55
22
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
});
|
|
61
|
-
this.zipfile.readEntry();
|
|
62
|
-
|
|
63
|
-
this.zipfile.on('close', () => {
|
|
64
|
-
if (!this.canceled) {
|
|
65
|
-
resolve();
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
this.zipfile.on('entry', async (entry) => {
|
|
70
|
-
if (this.canceled) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const fileName = this.extractFileName(entry);
|
|
75
|
-
if (fileName.startsWith('__MACOSX/')) {
|
|
76
|
-
this.zipfile.readEntry();
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const destDir = path.dirname(path.join(dir, fileName));
|
|
81
|
-
try {
|
|
82
|
-
await fs.mkdir(destDir, {recursive: true});
|
|
83
|
-
|
|
84
|
-
const canonicalDestDir = await fs.realpath(destDir);
|
|
85
|
-
const relativeDestDir = path.relative(dir, canonicalDestDir);
|
|
86
|
-
|
|
87
|
-
if (relativeDestDir.split(path.sep).includes('..')) {
|
|
88
|
-
new Error(
|
|
89
|
-
`Out of bound path "${canonicalDestDir}" found while processing file ${fileName}`
|
|
90
|
-
);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
await this.extractEntry(entry);
|
|
94
|
-
this.zipfile.readEntry();
|
|
95
|
-
} catch (err) {
|
|
96
|
-
this.canceled = true;
|
|
97
|
-
this.zipfile.close();
|
|
98
|
-
reject(err);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
async extractEntry(entry) {
|
|
105
|
-
if (this.canceled) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const {dir} = this.opts;
|
|
110
|
-
|
|
111
|
-
const fileName = this.extractFileName(entry);
|
|
112
|
-
const dest = path.join(dir, fileName);
|
|
113
|
-
|
|
114
|
-
// convert external file attr int into a fs stat mode int
|
|
115
|
-
const mode = (entry.externalFileAttributes >> 16) & 0xffff;
|
|
116
|
-
// check if it's a symlink or dir (using stat mode constants)
|
|
117
|
-
const isSymlink = (mode & IFMT) === IFLNK;
|
|
118
|
-
const isDir =
|
|
119
|
-
(mode & IFMT) === IFDIR ||
|
|
120
|
-
// Failsafe, borrowed from jsZip
|
|
121
|
-
fileName.endsWith('/') ||
|
|
122
|
-
// check for windows weird way of specifying a directory
|
|
123
|
-
// https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
|
|
124
|
-
(entry.versionMadeBy >> 8 === 0 && entry.externalFileAttributes === 16);
|
|
125
|
-
const procMode = this.getExtractedMode(mode, isDir) & 0o777;
|
|
126
|
-
// always ensure folders are created
|
|
127
|
-
const destDir = isDir ? dest : path.dirname(dest);
|
|
128
|
-
const mkdirOptions = {recursive: true};
|
|
129
|
-
if (isDir) {
|
|
130
|
-
mkdirOptions.mode = procMode;
|
|
131
|
-
}
|
|
132
|
-
await fs.mkdir(destDir, mkdirOptions);
|
|
133
|
-
if (isDir) {
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/** @type {(entry: yauzl.Entry) => Promise<NodeJS.ReadableStream>} */
|
|
138
|
-
const openReadStream = B.promisify(this.zipfile.openReadStream.bind(this.zipfile));
|
|
139
|
-
const readStream = await openReadStream(entry);
|
|
140
|
-
if (isSymlink) {
|
|
141
|
-
// @ts-ignore This typecast is ok
|
|
142
|
-
const link = await getStream(readStream);
|
|
143
|
-
await fs.symlink(link, dest);
|
|
144
|
-
} else {
|
|
145
|
-
await pipeline(readStream, fs.createWriteStream(dest, {mode: procMode}));
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
getExtractedMode(entryMode, isDir) {
|
|
150
|
-
const {defaultDirMode, defaultFileMode} = this.opts;
|
|
151
|
-
|
|
152
|
-
let mode = entryMode;
|
|
153
|
-
// Set defaults, if necessary
|
|
154
|
-
if (mode === 0) {
|
|
155
|
-
if (isDir) {
|
|
156
|
-
if (defaultDirMode) {
|
|
157
|
-
mode = parseInt(defaultDirMode, 10);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (!mode) {
|
|
161
|
-
mode = 0o755;
|
|
162
|
-
}
|
|
163
|
-
} else {
|
|
164
|
-
if (defaultFileMode) {
|
|
165
|
-
mode = parseInt(defaultFileMode, 10);
|
|
166
|
-
}
|
|
23
|
+
const ZIP_MAGIC = 'PK';
|
|
24
|
+
const IFMT = 0b1111000000000000;
|
|
25
|
+
const IFDIR = 0b0100000000000000;
|
|
26
|
+
const IFLNK = 0b1010000000000000;
|
|
167
27
|
|
|
168
|
-
|
|
169
|
-
mode = 0o644;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
28
|
+
// Internal extraction helpers are defined near the end of the file.
|
|
173
29
|
|
|
174
|
-
|
|
175
|
-
|
|
30
|
+
export interface ExtractAllOptions {
|
|
31
|
+
/**
|
|
32
|
+
* The encoding to use for extracted file names.
|
|
33
|
+
* For ZIP archives created on MacOS it is usually expected to be `utf8`.
|
|
34
|
+
* By default it is autodetected based on the entry metadata and is only needed to be set explicitly
|
|
35
|
+
* if the particular archive does not comply to the standards, which leads to corrupted file names
|
|
36
|
+
* after extraction. Only applicable if system unzip binary is NOT being used.
|
|
37
|
+
*/
|
|
38
|
+
fileNamesEncoding?: BufferEncoding;
|
|
39
|
+
/**
|
|
40
|
+
* If true, attempt to use system unzip; if this fails,
|
|
41
|
+
* fallback to the JS unzip implementation.
|
|
42
|
+
*/
|
|
43
|
+
useSystemUnzip?: boolean;
|
|
176
44
|
}
|
|
177
45
|
|
|
178
|
-
/**
|
|
179
|
-
* @typedef ExtractAllOptions
|
|
180
|
-
* @property {string} [fileNamesEncoding] The encoding to use for extracted file names.
|
|
181
|
-
* For ZIP archives created on MacOS it is usually expected to be `utf8`.
|
|
182
|
-
* By default it is autodetected based on the entry metadata and is only needed to be set explicitly
|
|
183
|
-
* if the particular archive does not comply to the standards, which leads to corrupted file names
|
|
184
|
-
* after extraction. Only applicable if system unzip binary is NOT being used.
|
|
185
|
-
* @property {boolean} [useSystemUnzip] If true, attempt to use system unzip; if this fails,
|
|
186
|
-
* fallback to the JS unzip implementation.
|
|
187
|
-
*/
|
|
188
|
-
|
|
189
46
|
/**
|
|
190
47
|
* Extract zipfile to a directory
|
|
191
48
|
*
|
|
192
|
-
* @param
|
|
193
|
-
* @param
|
|
194
|
-
* @param
|
|
49
|
+
* @param zipFilePath The full path to the source ZIP file
|
|
50
|
+
* @param destDir The full path to the destination folder
|
|
51
|
+
* @param opts Extraction options
|
|
195
52
|
*/
|
|
196
|
-
async function extractAllTo(
|
|
53
|
+
export async function extractAllTo(
|
|
54
|
+
zipFilePath: string,
|
|
55
|
+
destDir: string,
|
|
56
|
+
opts: ExtractAllOptions = {}
|
|
57
|
+
): Promise<void> {
|
|
197
58
|
if (!path.isAbsolute(destDir)) {
|
|
198
59
|
throw new Error(`Target path '${destDir}' is expected to be absolute`);
|
|
199
60
|
}
|
|
@@ -204,63 +65,36 @@ async function extractAllTo(zipFilePath, destDir, opts = /** @type {ExtractAllOp
|
|
|
204
65
|
try {
|
|
205
66
|
await extractWithSystemUnzip(zipFilePath, dir);
|
|
206
67
|
return;
|
|
207
|
-
} catch (err) {
|
|
68
|
+
} catch (err: any) {
|
|
208
69
|
log.warn('unzip failed; falling back to JS: %s', err.stderr || err.message);
|
|
209
70
|
}
|
|
210
71
|
}
|
|
211
72
|
const extractor = new ZipExtractor(zipFilePath, {
|
|
212
|
-
...opts,
|
|
73
|
+
...(opts as ExtractAllOptions & Partial<ZipExtractorOptions>),
|
|
213
74
|
dir,
|
|
214
75
|
});
|
|
215
76
|
await extractor.extract();
|
|
216
77
|
}
|
|
217
78
|
|
|
218
|
-
/**
|
|
219
|
-
* Executes system unzip (e.g., `/usr/bin/unzip`). If available, it is
|
|
220
|
-
* significantly faster than the JS implementation.
|
|
221
|
-
* By default all files in the destDir get overridden if already exist.
|
|
222
|
-
*
|
|
223
|
-
* @param {string} zipFilePath The full path to the source ZIP file
|
|
224
|
-
* @param {string} destDir The full path to the destination folder.
|
|
225
|
-
* This folder is expected to already exist before extracting the archive.
|
|
226
|
-
*/
|
|
227
|
-
async function extractWithSystemUnzip(zipFilePath, destDir) {
|
|
228
|
-
const isWindowsHost = isWindows();
|
|
229
|
-
let executablePath;
|
|
230
|
-
try {
|
|
231
|
-
executablePath = await getExecutablePath(isWindowsHost ? 'powershell.exe' : 'unzip');
|
|
232
|
-
} catch {
|
|
233
|
-
throw new Error('Could not find system unzip');
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (isWindowsHost) {
|
|
237
|
-
// on Windows we use PowerShell to unzip files
|
|
238
|
-
await exec(executablePath, [
|
|
239
|
-
'-command',
|
|
240
|
-
'Expand-Archive',
|
|
241
|
-
'-LiteralPath',
|
|
242
|
-
zipFilePath,
|
|
243
|
-
'-DestinationPath',
|
|
244
|
-
destDir,
|
|
245
|
-
'-Force',
|
|
246
|
-
]);
|
|
247
|
-
} else {
|
|
248
|
-
// -q means quiet (no stdout)
|
|
249
|
-
// -o means overwrite
|
|
250
|
-
// -d is the dest dir
|
|
251
|
-
await exec(executablePath, ['-q', '-o', zipFilePath, '-d', destDir]);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
79
|
/**
|
|
256
80
|
* Extract a single zip entry to a directory
|
|
257
81
|
*
|
|
258
|
-
* @
|
|
259
|
-
* @param
|
|
260
|
-
* @param
|
|
82
|
+
* @private
|
|
83
|
+
* @param zipFile The source ZIP stream
|
|
84
|
+
* @param entry The entry instance
|
|
85
|
+
* @param destDir The full path to the destination folder
|
|
261
86
|
*/
|
|
262
|
-
async function _extractEntryTo(
|
|
87
|
+
export async function _extractEntryTo(
|
|
88
|
+
zipFile: yauzl.ZipFile,
|
|
89
|
+
entry: yauzl.Entry,
|
|
90
|
+
destDir: string
|
|
91
|
+
): Promise<void> {
|
|
263
92
|
const dstPath = path.resolve(destDir, entry.fileName);
|
|
93
|
+
if (!isSubPath(dstPath, destDir)) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Out of bound path "${dstPath}" found while processing file ${entry.fileName}`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
264
98
|
|
|
265
99
|
// Create dest directory if doesn't exist already
|
|
266
100
|
if (entry.fileName.endsWith('/')) {
|
|
@@ -274,54 +108,59 @@ async function _extractEntryTo(zipFile, entry, destDir) {
|
|
|
274
108
|
|
|
275
109
|
// Create a write stream
|
|
276
110
|
const writeStream = createWriteStream(dstPath, {flags: 'w'});
|
|
277
|
-
const writeStreamPromise = new
|
|
111
|
+
const writeStreamPromise = new Promise<void>((resolve, reject) => {
|
|
278
112
|
writeStream.once('finish', resolve);
|
|
279
113
|
writeStream.once('error', reject);
|
|
280
114
|
});
|
|
281
115
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const zipReadStreamPromise = new B((resolve, reject) => {
|
|
116
|
+
const openReadStream = promisify(zipFile.openReadStream.bind(zipFile)) as (
|
|
117
|
+
entry: yauzl.Entry
|
|
118
|
+
) => Promise<NodeJS.ReadableStream>;
|
|
119
|
+
const zipReadStream = await openReadStream(entry);
|
|
120
|
+
const zipReadStreamPromise = new Promise<void>((resolve, reject) => {
|
|
288
121
|
zipReadStream.once('end', resolve);
|
|
289
122
|
zipReadStream.once('error', reject);
|
|
290
123
|
});
|
|
291
124
|
zipReadStream.pipe(writeStream);
|
|
292
125
|
|
|
293
126
|
// Wait for the zipReadStream and writeStream to end before returning
|
|
294
|
-
|
|
127
|
+
await Promise.all([zipReadStreamPromise, writeStreamPromise]);
|
|
295
128
|
}
|
|
296
129
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
130
|
+
export interface ZipEntry {
|
|
131
|
+
/** The actual entry instance */
|
|
132
|
+
entry: yauzl.Entry;
|
|
133
|
+
/**
|
|
134
|
+
* Async function which accepts the destination folder path
|
|
135
|
+
* and extracts this entry into it.
|
|
136
|
+
*/
|
|
137
|
+
extractEntryTo: (destDir: string) => Promise<void>;
|
|
138
|
+
}
|
|
303
139
|
|
|
304
140
|
/**
|
|
305
141
|
* Get entries for a zip folder
|
|
306
142
|
*
|
|
307
|
-
* @param
|
|
308
|
-
* @param
|
|
143
|
+
* @param zipFilePath The full path to the source ZIP file
|
|
144
|
+
* @param onEntry Callback when entry is read.
|
|
309
145
|
* The callback is expected to accept one argument of ZipEntry type.
|
|
310
|
-
* The iteration through the source zip file will
|
|
146
|
+
* The iteration through the source zip file will be terminated as soon as
|
|
311
147
|
* the result of this function equals to `false`.
|
|
312
148
|
*/
|
|
313
|
-
async function readEntries(
|
|
149
|
+
export async function readEntries(
|
|
150
|
+
zipFilePath: string,
|
|
151
|
+
onEntry: (entry: ZipEntry) => boolean | void | Promise<boolean | void>
|
|
152
|
+
): Promise<void> {
|
|
314
153
|
// Open a zip file and start reading entries
|
|
315
154
|
const zipfile = await openZip(zipFilePath, {lazyEntries: true});
|
|
316
|
-
const zipReadStreamPromise = new
|
|
155
|
+
const zipReadStreamPromise = new Promise<void>((resolve, reject) => {
|
|
317
156
|
zipfile.once('end', resolve);
|
|
318
157
|
zipfile.once('error', reject);
|
|
319
158
|
|
|
320
159
|
// On each entry, call 'onEntry' and then read the next entry
|
|
321
|
-
zipfile.on('entry', async (entry) => {
|
|
160
|
+
zipfile.on('entry', async (entry: yauzl.Entry) => {
|
|
322
161
|
const res = await onEntry({
|
|
323
162
|
entry,
|
|
324
|
-
extractEntryTo: async (destDir) => await _extractEntryTo(zipfile, entry, destDir),
|
|
163
|
+
extractEntryTo: async (destDir: string) => await _extractEntryTo(zipfile, entry, destDir),
|
|
325
164
|
});
|
|
326
165
|
if (res === false) {
|
|
327
166
|
return zipfile.emit('end');
|
|
@@ -332,46 +171,53 @@ async function readEntries(zipFilePath, onEntry) {
|
|
|
332
171
|
zipfile.readEntry();
|
|
333
172
|
|
|
334
173
|
// Wait for the entries to finish being iterated through
|
|
335
|
-
|
|
174
|
+
await zipReadStreamPromise;
|
|
336
175
|
}
|
|
337
176
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
177
|
+
export interface ZipOptions {
|
|
178
|
+
/** Whether to encode the resulting archive to a base64-encoded string */
|
|
179
|
+
encodeToBase64?: boolean;
|
|
180
|
+
/** Whether to log the actual archiver performance */
|
|
181
|
+
isMetered?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* The maximum size of the resulting archive in bytes.
|
|
184
|
+
* This is set to 1GB by default, because Appium limits the maximum HTTP body size to 1GB.
|
|
185
|
+
* Also, the NodeJS heap size must be enough to keep the resulting object
|
|
186
|
+
* (usually this size is limited to 1.4 GB)
|
|
187
|
+
*/
|
|
188
|
+
maxSize?: number;
|
|
189
|
+
/**
|
|
190
|
+
* The compression level.
|
|
191
|
+
* The maximum level is 9 (the best compression, worst performance).
|
|
192
|
+
* The minimum compression level is 0 (no compression).
|
|
193
|
+
*/
|
|
194
|
+
level?: number;
|
|
195
|
+
}
|
|
353
196
|
|
|
354
197
|
/**
|
|
355
198
|
* Converts contents of local directory to an in-memory .zip buffer
|
|
356
199
|
*
|
|
357
|
-
* @param
|
|
358
|
-
* @param
|
|
359
|
-
* @returns
|
|
200
|
+
* @param srcPath The full path to the folder or file being zipped
|
|
201
|
+
* @param opts Zipping options
|
|
202
|
+
* @returns Zipped (and encoded if `encodeToBase64` is truthy)
|
|
360
203
|
* content of the source path as memory buffer
|
|
361
204
|
* @throws {Error} if there was an error while reading the source
|
|
362
205
|
* or the source is too big
|
|
363
206
|
*/
|
|
364
|
-
async function toInMemoryZip(
|
|
207
|
+
export async function toInMemoryZip(
|
|
208
|
+
srcPath: string,
|
|
209
|
+
opts: ZipOptions = {}
|
|
210
|
+
): Promise<Buffer> {
|
|
365
211
|
if (!(await fs.exists(srcPath))) {
|
|
366
212
|
throw new Error(`No such file or folder: ${srcPath}`);
|
|
367
213
|
}
|
|
368
214
|
|
|
369
215
|
const {isMetered = true, encodeToBase64 = false, maxSize = 1 * GiB, level = 9} = opts;
|
|
370
|
-
const resultBuffers = [];
|
|
216
|
+
const resultBuffers: Buffer[] = [];
|
|
371
217
|
let resultBuffersSize = 0;
|
|
372
218
|
// Create a writable stream that zip buffers will be streamed to
|
|
373
219
|
const resultWriteStream = new stream.Writable({
|
|
374
|
-
write:
|
|
220
|
+
write(buffer: Buffer, _encoding: string, next: (err?: Error) => void) {
|
|
375
221
|
resultBuffers.push(buffer);
|
|
376
222
|
resultBuffersSize += buffer.length;
|
|
377
223
|
if (maxSize > 0 && resultBuffersSize > maxSize) {
|
|
@@ -391,10 +237,10 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
|
|
|
391
237
|
const archive = archiver('zip', {
|
|
392
238
|
zlib: {level},
|
|
393
239
|
});
|
|
394
|
-
let srcSize = null;
|
|
240
|
+
let srcSize: number | null = null;
|
|
395
241
|
const base64EncoderStream = encodeToBase64 ? new Base64Encode() : null;
|
|
396
|
-
const resultWriteStreamPromise = new
|
|
397
|
-
resultWriteStream.once('error', (e) => {
|
|
242
|
+
const resultWriteStreamPromise = new Promise<void>((resolve, reject) => {
|
|
243
|
+
resultWriteStream.once('error', (e: Error) => {
|
|
398
244
|
if (base64EncoderStream) {
|
|
399
245
|
archive.unpipe(base64EncoderStream);
|
|
400
246
|
base64EncoderStream.unpipe(resultWriteStream);
|
|
@@ -410,9 +256,11 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
|
|
|
410
256
|
resolve();
|
|
411
257
|
});
|
|
412
258
|
});
|
|
413
|
-
const archiveStreamPromise = new
|
|
259
|
+
const archiveStreamPromise = new Promise<void>((resolve, reject) => {
|
|
414
260
|
archive.once('finish', resolve);
|
|
415
|
-
archive.once('error', (e) =>
|
|
261
|
+
archive.once('error', (e: Error) =>
|
|
262
|
+
reject(new Error(`Failed to archive '${srcPath}': ${e.message}`))
|
|
263
|
+
);
|
|
416
264
|
});
|
|
417
265
|
const timer = isMetered ? new Timer().start() : null;
|
|
418
266
|
if ((await fs.stat(srcPath)).isDirectory()) {
|
|
@@ -431,7 +279,7 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
|
|
|
431
279
|
archive.finalize();
|
|
432
280
|
|
|
433
281
|
// Wait for the streams to finish
|
|
434
|
-
await
|
|
282
|
+
await Promise.all([archiveStreamPromise, resultWriteStreamPromise]);
|
|
435
283
|
|
|
436
284
|
if (timer) {
|
|
437
285
|
log.debug(
|
|
@@ -449,10 +297,10 @@ async function toInMemoryZip(srcPath, opts = /** @type {ZipOptions} */ ({})) {
|
|
|
449
297
|
/**
|
|
450
298
|
* Verifies whether the given file is a valid ZIP archive
|
|
451
299
|
*
|
|
452
|
-
* @param
|
|
300
|
+
* @param filePath - Full path to the file
|
|
453
301
|
* @throws {Error} If the file does not exist or is not a valid ZIP archive
|
|
454
302
|
*/
|
|
455
|
-
async function assertValidZip(filePath) {
|
|
303
|
+
export async function assertValidZip(filePath: string): Promise<boolean> {
|
|
456
304
|
if (!(await fs.exists(filePath))) {
|
|
457
305
|
throw new Error(`The file at '${filePath}' does not exist`);
|
|
458
306
|
}
|
|
@@ -478,48 +326,51 @@ async function assertValidZip(filePath) {
|
|
|
478
326
|
}
|
|
479
327
|
}
|
|
480
328
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
329
|
+
export interface ZipCompressionOptions {
|
|
330
|
+
/**
|
|
331
|
+
* Compression level in range 0..9
|
|
332
|
+
* (greater numbers mean better compression, but longer processing time)
|
|
333
|
+
*/
|
|
334
|
+
level?: number;
|
|
335
|
+
}
|
|
486
336
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
337
|
+
export interface ZipSourceOptions {
|
|
338
|
+
/** GLOB pattern for compression */
|
|
339
|
+
pattern?: string;
|
|
340
|
+
/** The source root folder (the parent folder of the destination file by default) */
|
|
341
|
+
cwd?: string;
|
|
342
|
+
/** The list of ignored patterns */
|
|
343
|
+
ignore?: string[];
|
|
344
|
+
}
|
|
494
345
|
|
|
495
346
|
/**
|
|
496
347
|
* Creates an archive based on the given glob pattern
|
|
497
348
|
*
|
|
498
|
-
* @param
|
|
499
|
-
* @param
|
|
500
|
-
* @param
|
|
349
|
+
* @param dstPath - The resulting archive path
|
|
350
|
+
* @param src - Source options
|
|
351
|
+
* @param opts - Compression options
|
|
501
352
|
* @throws {Error} If there was an error while creating the archive
|
|
502
353
|
*/
|
|
503
|
-
async function toArchive(
|
|
504
|
-
dstPath,
|
|
505
|
-
src
|
|
506
|
-
opts
|
|
507
|
-
) {
|
|
354
|
+
export async function toArchive(
|
|
355
|
+
dstPath: string,
|
|
356
|
+
src: ZipSourceOptions = {},
|
|
357
|
+
opts: ZipCompressionOptions = {}
|
|
358
|
+
): Promise<void> {
|
|
508
359
|
const {level = 9} = opts;
|
|
509
360
|
const {pattern = '**/*', cwd = path.dirname(dstPath), ignore = []} = src;
|
|
510
361
|
const archive = archiver('zip', {zlib: {level}});
|
|
511
|
-
const
|
|
512
|
-
|
|
362
|
+
const outStream = fs.createWriteStream(dstPath);
|
|
363
|
+
await new Promise<void>((resolve, reject) => {
|
|
513
364
|
archive
|
|
514
365
|
.glob(pattern, {
|
|
515
366
|
cwd,
|
|
516
367
|
ignore,
|
|
517
368
|
})
|
|
518
369
|
.on('error', reject)
|
|
519
|
-
.pipe(
|
|
520
|
-
|
|
521
|
-
.on('error', (e) => {
|
|
522
|
-
archive.unpipe(
|
|
370
|
+
.pipe(outStream);
|
|
371
|
+
outStream
|
|
372
|
+
.on('error', (e: Error) => {
|
|
373
|
+
archive.unpipe(outStream);
|
|
523
374
|
archive.abort();
|
|
524
375
|
archive.destroy();
|
|
525
376
|
reject(e);
|
|
@@ -529,22 +380,215 @@ async function toArchive(
|
|
|
529
380
|
});
|
|
530
381
|
}
|
|
531
382
|
|
|
383
|
+
interface ZipExtractorOptions {
|
|
384
|
+
dir: string;
|
|
385
|
+
fileNamesEncoding?: BufferEncoding;
|
|
386
|
+
defaultDirMode?: string;
|
|
387
|
+
defaultFileMode?: string;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// This class is mostly copied from https://github.com/maxogden/extract-zip/blob/master/index.js
|
|
391
|
+
class ZipExtractor {
|
|
392
|
+
zipfile!: yauzl.ZipFile;
|
|
393
|
+
private readonly zipPath: string;
|
|
394
|
+
private readonly opts: ZipExtractorOptions;
|
|
395
|
+
private canceled = false;
|
|
396
|
+
|
|
397
|
+
constructor(sourcePath: string, opts: ZipExtractorOptions) {
|
|
398
|
+
this.zipPath = sourcePath;
|
|
399
|
+
this.opts = opts;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private extractFileName(entry: yauzl.Entry): string {
|
|
403
|
+
if (Buffer.isBuffer(entry.fileName)) {
|
|
404
|
+
return entry.fileName.toString(this.opts.fileNamesEncoding);
|
|
405
|
+
}
|
|
406
|
+
return entry.fileName;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async extract(): Promise<void> {
|
|
410
|
+
const {dir, fileNamesEncoding} = this.opts;
|
|
411
|
+
this.zipfile = await openZip(this.zipPath, {
|
|
412
|
+
lazyEntries: true,
|
|
413
|
+
// https://github.com/thejoshwolfe/yauzl/commit/cc7455ac789ba84973184e5ebde0581cdc4c3b39#diff-04c6e90faac2675aa89e2176d2eec7d8R95
|
|
414
|
+
decodeStrings: !fileNamesEncoding,
|
|
415
|
+
});
|
|
416
|
+
this.canceled = false;
|
|
417
|
+
|
|
418
|
+
return new Promise<void>((resolve, reject) => {
|
|
419
|
+
this.zipfile.on('error', (err: Error) => {
|
|
420
|
+
this.canceled = true;
|
|
421
|
+
reject(err);
|
|
422
|
+
});
|
|
423
|
+
this.zipfile.readEntry();
|
|
424
|
+
|
|
425
|
+
this.zipfile.on('close', () => {
|
|
426
|
+
if (!this.canceled) {
|
|
427
|
+
resolve();
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
this.zipfile.on('entry', async (entry: yauzl.Entry) => {
|
|
432
|
+
if (this.canceled) {
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const fileName = this.extractFileName(entry);
|
|
437
|
+
if (fileName.startsWith('__MACOSX/')) {
|
|
438
|
+
this.zipfile.readEntry();
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const destDir = path.dirname(path.join(dir, fileName));
|
|
443
|
+
try {
|
|
444
|
+
await fs.mkdir(destDir, {recursive: true});
|
|
445
|
+
|
|
446
|
+
const canonicalDestDir = await fs.realpath(destDir);
|
|
447
|
+
const relativeDestDir = path.relative(dir, canonicalDestDir);
|
|
448
|
+
|
|
449
|
+
if (relativeDestDir.split(path.sep).includes('..')) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
`Out of bound path "${canonicalDestDir}" found while processing file ${fileName}`
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
await this.extractEntry(entry);
|
|
456
|
+
this.zipfile.readEntry();
|
|
457
|
+
} catch (err) {
|
|
458
|
+
this.canceled = true;
|
|
459
|
+
this.zipfile.close();
|
|
460
|
+
reject(err);
|
|
461
|
+
}
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async extractEntry(entry: yauzl.Entry): Promise<void> {
|
|
467
|
+
if (this.canceled) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const {dir} = this.opts;
|
|
472
|
+
|
|
473
|
+
const fileName = this.extractFileName(entry);
|
|
474
|
+
const dest = path.join(dir, fileName);
|
|
475
|
+
|
|
476
|
+
// convert external file attr int into a fs stat mode int
|
|
477
|
+
const mode = (entry.externalFileAttributes >> 16) & 0xffff;
|
|
478
|
+
// check if it's a symlink or dir (using stat mode constants)
|
|
479
|
+
const isSymlink = (mode & IFMT) === IFLNK;
|
|
480
|
+
const isDir =
|
|
481
|
+
(mode & IFMT) === IFDIR ||
|
|
482
|
+
// Failsafe, borrowed from jsZip
|
|
483
|
+
fileName.endsWith('/') ||
|
|
484
|
+
// check for windows weird way of specifying a directory
|
|
485
|
+
// https://github.com/maxogden/extract-zip/issues/13#issuecomment-154494566
|
|
486
|
+
(entry.versionMadeBy >> 8 === 0 && entry.externalFileAttributes === 16);
|
|
487
|
+
const procMode = this.getExtractedMode(mode, isDir) & 0o777;
|
|
488
|
+
// always ensure folders are created
|
|
489
|
+
const destDir = isDir ? dest : path.dirname(dest);
|
|
490
|
+
const mkdirOptions: Parameters<typeof fs.mkdir>[1] = {recursive: true};
|
|
491
|
+
if (isDir) {
|
|
492
|
+
mkdirOptions.mode = procMode;
|
|
493
|
+
}
|
|
494
|
+
await fs.mkdir(destDir, mkdirOptions);
|
|
495
|
+
if (isDir) {
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const openReadStream = promisify(
|
|
500
|
+
this.zipfile.openReadStream.bind(this.zipfile)
|
|
501
|
+
) as (entry: yauzl.Entry) => Promise<NodeJS.ReadableStream>;
|
|
502
|
+
const readStream = await openReadStream(entry);
|
|
503
|
+
if (isSymlink) {
|
|
504
|
+
const link = await getStream(readStream);
|
|
505
|
+
await fs.symlink(link, dest);
|
|
506
|
+
} else {
|
|
507
|
+
await pipeline(readStream, fs.createWriteStream(dest, {mode: procMode}));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private getExtractedMode(entryMode: number, isDir: boolean): number {
|
|
512
|
+
const {defaultDirMode, defaultFileMode} = this.opts;
|
|
513
|
+
|
|
514
|
+
let mode = entryMode;
|
|
515
|
+
// Set defaults, if necessary
|
|
516
|
+
if (mode === 0) {
|
|
517
|
+
if (isDir) {
|
|
518
|
+
if (defaultDirMode) {
|
|
519
|
+
mode = parseInt(defaultDirMode, 10);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (!mode) {
|
|
523
|
+
mode = 0o755;
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
if (defaultFileMode) {
|
|
527
|
+
mode = parseInt(defaultFileMode, 10);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (!mode) {
|
|
531
|
+
mode = 0o644;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return mode;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Executes system unzip (e.g., `/usr/bin/unzip`). If available, it is
|
|
542
|
+
* significantly faster than the JS implementation.
|
|
543
|
+
* By default all files in the destDir get overridden if already exist.
|
|
544
|
+
*
|
|
545
|
+
* @param zipFilePath The full path to the source ZIP file
|
|
546
|
+
* @param destDir The full path to the destination folder.
|
|
547
|
+
* This folder is expected to already exist before extracting the archive.
|
|
548
|
+
*/
|
|
549
|
+
async function extractWithSystemUnzip(zipFilePath: string, destDir: string): Promise<void> {
|
|
550
|
+
const isWindowsHost = isWindows();
|
|
551
|
+
let executablePath: string;
|
|
552
|
+
try {
|
|
553
|
+
executablePath = await getExecutablePath(isWindowsHost ? 'powershell.exe' : 'unzip');
|
|
554
|
+
} catch {
|
|
555
|
+
throw new Error('Could not find system unzip');
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (isWindowsHost) {
|
|
559
|
+
// on Windows we use PowerShell to unzip files
|
|
560
|
+
await exec(executablePath, [
|
|
561
|
+
'-command',
|
|
562
|
+
'Expand-Archive',
|
|
563
|
+
'-LiteralPath',
|
|
564
|
+
zipFilePath,
|
|
565
|
+
'-DestinationPath',
|
|
566
|
+
destDir,
|
|
567
|
+
'-Force',
|
|
568
|
+
]);
|
|
569
|
+
} else {
|
|
570
|
+
// -q means quiet (no stdout)
|
|
571
|
+
// -o means overwrite
|
|
572
|
+
// -d is the dest dir
|
|
573
|
+
await exec(executablePath, ['-q', '-o', zipFilePath, '-d', destDir]);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
532
577
|
/**
|
|
533
578
|
* Finds and memoizes the full path to the given executable.
|
|
534
579
|
* Rejects if it is not found.
|
|
535
580
|
*/
|
|
536
581
|
const getExecutablePath = _.memoize(
|
|
537
582
|
/**
|
|
538
|
-
* @returns
|
|
583
|
+
* @returns Full Path to the executable
|
|
539
584
|
*/
|
|
540
|
-
async function getExecutablePath(binaryName) {
|
|
585
|
+
async function getExecutablePath(binaryName: string): Promise<string> {
|
|
541
586
|
const fullPath = await fs.which(binaryName);
|
|
542
587
|
log.debug(`Found '${binaryName}' at '${fullPath}'`);
|
|
543
588
|
return fullPath;
|
|
544
589
|
}
|
|
545
590
|
);
|
|
546
591
|
|
|
547
|
-
export {extractAllTo, readEntries, toInMemoryZip, _extractEntryTo, assertValidZip, toArchive};
|
|
548
592
|
export default {
|
|
549
593
|
extractAllTo,
|
|
550
594
|
readEntries,
|
|
@@ -552,3 +596,4 @@ export default {
|
|
|
552
596
|
assertValidZip,
|
|
553
597
|
toArchive,
|
|
554
598
|
};
|
|
599
|
+
|