zip.ts 7.0 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';
J
Joao Moreno 已提交
10
import { nfcall, ninvoke, 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;

S
Sandeep Somavarapu 已提交
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) {
B
Benjamin Pasero 已提交
52
	let 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
		}
	});

J
Johannes Rieken 已提交
89
	return Promise.resolve(mkdirp(targetDirName, undefined, token)).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);
R
Rob Lourens 已提交
152
				last = createCancelablePromise(token => mkdirp(targetFileName, undefined, token).then(() => readNextEntry(token)).then(undefined, e));
153 154 155 156 157 158
				return;
			}

			const stream = ninvoke(zipfile, zipfile.openReadStream, entry);
			const mode = modeFromEntry(entry);

J
Joao Moreno 已提交
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> {
R
Rob Lourens 已提交
165 166
	return nfcall<ZipFile>(_openZip, zipFile, lazy ? { lazyEntries: true } : undefined)
		.then(undefined, err => Promise.reject(toExtractError(err)));
E
Erich Gamma 已提交
167 168
}

169 170 171 172 173 174
export interface IFile {
	path: string;
	contents?: Buffer | string;
	localPath?: string;
}

S
Sandeep Somavarapu 已提交
175 176
export function zip(zipPath: string, files: IFile[]): Promise<string> {
	return new Promise<string>((c, e) => {
177
		const zip = new yazl.ZipFile();
M
Matt Bierner 已提交
178 179 180 181 182 183 184
		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);
			}
		});
185 186 187 188 189 190 191 192 193 194 195
		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 已提交
196
export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, token: CancellationToken): Promise<void> {
J
Johannes Rieken 已提交
197
	const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
E
Erich Gamma 已提交
198

S
Sandeep Somavarapu 已提交
199
	let promise = openZip(zipPath, true);
E
Erich Gamma 已提交
200 201

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

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

S
Sandeep Somavarapu 已提交
208
function read(zipPath: string, filePath: string): Promise<Readable> {
J
Joao Moreno 已提交
209
	return openZip(zipPath).then(zipfile => {
S
Sandeep Somavarapu 已提交
210
		return new Promise<Readable>((c, e) => {
E
Erich Gamma 已提交
211 212
			zipfile.on('entry', (entry: Entry) => {
				if (entry.fileName === filePath) {
213
					ninvoke<Readable>(zipfile, zipfile.openReadStream, entry).then(stream => c(stream), err => e(err));
E
Erich Gamma 已提交
214 215 216 217 218 219 220 221
				}
			});

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

S
Sandeep Somavarapu 已提交
222
export function buffer(zipPath: string, filePath: string): Promise<Buffer> {
E
Erich Gamma 已提交
223
	return read(zipPath, filePath).then(stream => {
S
Sandeep Somavarapu 已提交
224
		return new Promise<Buffer>((c, e) => {
B
Benjamin Pasero 已提交
225
			const buffers: Buffer[] = [];
E
Erich Gamma 已提交
226
			stream.once('error', e);
J
Joao Moreno 已提交
227
			stream.on('data', b => buffers.push(b as Buffer));
E
Erich Gamma 已提交
228 229 230 231
			stream.on('end', () => c(Buffer.concat(buffers)));
		});
	});
}