zip.ts 7.2 KB
Newer Older
E
Erich Gamma 已提交
1 2 3 4 5
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

6
import * as nls from 'vs/nls';
7
import * as path from 'vs/base/common/path';
8
import { createWriteStream, WriteStream } from 'fs';
E
Erich Gamma 已提交
9
import { Readable } from 'stream';
10
import { Sequencer, createCancelablePromise } from 'vs/base/common/async';
E
Erich Gamma 已提交
11
import { mkdirp, rimraf } from 'vs/base/node/pfs';
J
Joao Moreno 已提交
12
import { open as _openZip, Entry, ZipFile } from 'yauzl';
13
import * as yazl from 'yazl';
14
import { CancellationToken } from 'vs/base/common/cancellation';
J
Joao Moreno 已提交
15
import { Event } from 'vs/base/common/event';
E
Erich Gamma 已提交
16 17 18 19

export interface IExtractOptions {
	overwrite?: boolean;

20
	/**
E
Erich Gamma 已提交
21 22 23 24 25 26 27 28 29 30
	 * Source path within the ZIP archive. Only the files contained in this
	 * path will be extracted.
	 */
	sourcePath?: string;
}

interface IOptions {
	sourcePathRegex: RegExp;
}

S
Sandeep Somavarapu 已提交
31
export type ExtractErrorType = 'CorruptZip' | 'Incomplete';
J
Joao Moreno 已提交
32 33 34

export class ExtractError extends Error {

M
Matt Bierner 已提交
35
	readonly type?: ExtractErrorType;
J
Joao Moreno 已提交
36 37
	readonly cause: Error;

M
Matt Bierner 已提交
38
	constructor(type: ExtractErrorType | undefined, cause: Error) {
J
Joao Moreno 已提交
39 40 41
		let message = cause.message;

		switch (type) {
S
Sandeep Somavarapu 已提交
42
			case 'CorruptZip': message = `Corrupt ZIP: ${message}`; break;
J
Joao Moreno 已提交
43 44 45 46 47 48 49 50
		}

		super(message);
		this.type = type;
		this.cause = cause;
	}
}

E
Erich Gamma 已提交
51
function modeFromEntry(entry: Entry) {
52
	const attr = entry.externalFileAttributes >> 16 || 33188;
E
Erich Gamma 已提交
53 54 55 56 57 58

	return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
		.map(mask => attr & mask)
		.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
}

J
Joao Moreno 已提交
59
function toExtractError(err: Error): ExtractError {
S
Sandeep Somavarapu 已提交
60 61 62 63
	if (err instanceof ExtractError) {
		return err;
	}

R
Rob Lourens 已提交
64
	let type: ExtractErrorType | undefined = undefined;
J
Joao Moreno 已提交
65 66

	if (/end of central directory record signature not found/.test(err.message)) {
S
Sandeep Somavarapu 已提交
67
		type = 'CorruptZip';
J
Joao Moreno 已提交
68 69 70 71 72
	}

	return new ExtractError(type, err);
}

S
Sandeep Somavarapu 已提交
73
function extractEntry(stream: Readable, fileName: string, mode: number, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
E
Erich Gamma 已提交
74 75
	const dirName = path.dirname(fileName);
	const targetDirName = path.join(targetPath, dirName);
S
Sandeep Somavarapu 已提交
76
	if (targetDirName.indexOf(targetPath) !== 0) {
S
Sandeep Somavarapu 已提交
77
		return Promise.reject(new Error(nls.localize('invalid file', "Error extracting {0}. Invalid file.", fileName)));
S
Sandeep Somavarapu 已提交
78
	}
E
Erich Gamma 已提交
79 80
	const targetFileName = path.join(targetPath, fileName);

81
	let istream: WriteStream;
82

J
Joao Moreno 已提交
83
	Event.once(token.onCancellationRequested)(() => {
84
		if (istream) {
85
			istream.destroy();
86 87 88
		}
	});

89
	return Promise.resolve(mkdirp(targetDirName)).then(() => new Promise<void>((c, e) => {
90 91 92 93
		if (token.isCancellationRequested) {
			return;
		}

S
Sandeep Somavarapu 已提交
94 95
		try {
			istream = createWriteStream(targetFileName, { mode });
M
Matt Bierner 已提交
96
			istream.once('close', () => c());
S
Sandeep Somavarapu 已提交
97 98 99 100 101 102
			istream.once('error', e);
			stream.once('error', e);
			stream.pipe(istream);
		} catch (error) {
			e(error);
		}
103
	}));
E
Erich Gamma 已提交
104 105
}

S
Sandeep Somavarapu 已提交
106
function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, token: CancellationToken): Promise<void> {
M
Matt Bierner 已提交
107
	let last = createCancelablePromise<void>(() => Promise.resolve());
S
Sandeep Somavarapu 已提交
108
	let extractedEntriesCount = 0;
109

J
Joao Moreno 已提交
110
	Event.once(token.onCancellationRequested)(() => {
111 112 113
		last.cancel();
		zipfile.close();
	});
E
Erich Gamma 已提交
114

115
	return new Promise((c, e) => {
J
Joao Moreno 已提交
116
		const throttler = new Sequencer();
117

118 119 120 121
		const readNextEntry = (token: CancellationToken) => {
			if (token.isCancellationRequested) {
				return;
			}
122

123
			extractedEntriesCount++;
S
Sandeep Somavarapu 已提交
124
			zipfile.readEntry();
125 126 127 128 129
		};

		zipfile.once('error', e);
		zipfile.once('close', () => last.then(() => {
			if (token.isCancellationRequested || zipfile.entryCount === extractedEntriesCount) {
M
Matt Bierner 已提交
130
				c();
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
			} else {
				e(new ExtractError('Incomplete', new Error(nls.localize('incompleteExtract', "Incomplete. Found {0} of {1} entries", extractedEntriesCount, zipfile.entryCount))));
			}
		}, e));
		zipfile.readEntry();
		zipfile.on('entry', (entry: Entry) => {

			if (token.isCancellationRequested) {
				return;
			}

			if (!options.sourcePathRegex.test(entry.fileName)) {
				readNextEntry(token);
				return;
			}

			const fileName = entry.fileName.replace(options.sourcePathRegex, '');

			// directory file names end with '/'
			if (/\/$/.test(fileName)) {
				const targetFileName = path.join(targetPath, fileName);
152
				last = createCancelablePromise(token => mkdirp(targetFileName).then(() => readNextEntry(token)).then(undefined, e));
153 154 155
				return;
			}

156
			const stream = openZipStream(zipfile, entry);
157 158
			const mode = modeFromEntry(entry);

159
			last = createCancelablePromise(token => throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options, token).then(() => readNextEntry(token)))).then(null, e));
E
Erich Gamma 已提交
160
		});
161
	});
J
Joao Moreno 已提交
162 163
}

S
Sandeep Somavarapu 已提交
164
function openZip(zipFile: string, lazy: boolean = false): Promise<ZipFile> {
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
	return new Promise((resolve, reject) => {
		_openZip(zipFile, lazy ? { lazyEntries: true } : undefined, (error?: Error, zipfile?: ZipFile) => {
			if (error) {
				reject(toExtractError(error));
			} else {
				resolve(zipfile);
			}
		});
	});
}

function openZipStream(zipFile: ZipFile, entry: Entry): Promise<Readable> {
	return new Promise((resolve, reject) => {
		zipFile.openReadStream(entry, (error?: Error, stream?: Readable) => {
			if (error) {
				reject(toExtractError(error));
			} else {
				resolve(stream);
			}
		});
	});
E
Erich Gamma 已提交
186 187
}

188 189 190 191 192 193
export interface IFile {
	path: string;
	contents?: Buffer | string;
	localPath?: string;
}

S
Sandeep Somavarapu 已提交
194 195
export function zip(zipPath: string, files: IFile[]): Promise<string> {
	return new Promise<string>((c, e) => {
196
		const zip = new yazl.ZipFile();
M
Matt Bierner 已提交
197 198 199 200 201 202 203
		files.forEach(f => {
			if (f.contents) {
				zip.addBuffer(typeof f.contents === 'string' ? Buffer.from(f.contents, 'utf8') : f.contents, f.path);
			} else if (f.localPath) {
				zip.addFile(f.localPath, f.path);
			}
		});
204 205 206 207 208 209 210 211 212 213 214
		zip.end();

		const zipStream = createWriteStream(zipPath);
		zip.outputStream.pipe(zipStream);

		zip.outputStream.once('error', e);
		zipStream.once('error', e);
		zipStream.once('finish', () => c(zipPath));
	});
}

S
Sandeep Somavarapu 已提交
215
export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> {
J
Johannes Rieken 已提交
216
	const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
E
Erich Gamma 已提交
217

S
Sandeep Somavarapu 已提交
218
	let promise = openZip(zipPath, true);
E
Erich Gamma 已提交
219 220

	if (options.overwrite) {
J
Joao Moreno 已提交
221
		promise = promise.then(zipfile => rimraf(targetPath).then(() => zipfile));
E
Erich Gamma 已提交
222 223
	}

S
Sandeep Somavarapu 已提交
224
	return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, token));
E
Erich Gamma 已提交
225 226
}

S
Sandeep Somavarapu 已提交
227
function read(zipPath: string, filePath: string): Promise<Readable> {
J
Joao Moreno 已提交
228
	return openZip(zipPath).then(zipfile => {
S
Sandeep Somavarapu 已提交
229
		return new Promise<Readable>((c, e) => {
E
Erich Gamma 已提交
230 231
			zipfile.on('entry', (entry: Entry) => {
				if (entry.fileName === filePath) {
232
					openZipStream(zipfile, entry).then(stream => c(stream), err => e(err));
E
Erich Gamma 已提交
233 234 235 236 237 238 239 240
				}
			});

			zipfile.once('close', () => e(new Error(nls.localize('notFound', "{0} not found inside zip.", filePath))));
		});
	});
}

S
Sandeep Somavarapu 已提交
241
export function buffer(zipPath: string, filePath: string): Promise<Buffer> {
E
Erich Gamma 已提交
242
	return read(zipPath, filePath).then(stream => {
S
Sandeep Somavarapu 已提交
243
		return new Promise<Buffer>((c, e) => {
B
Benjamin Pasero 已提交
244
			const buffers: Buffer[] = [];
E
Erich Gamma 已提交
245
			stream.once('error', e);
B
Benjamin Pasero 已提交
246
			stream.on('data', (b: Buffer) => buffers.push(b));
E
Erich Gamma 已提交
247 248 249 250
			stream.on('end', () => c(Buffer.concat(buffers)));
		});
	});
}