zip.ts 5.8 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';
E
Erich Gamma 已提交
7
import * as path from 'path';
8
import { createWriteStream, WriteStream } from 'fs';
E
Erich Gamma 已提交
9
import { Readable } from 'stream';
10
import { nfcall, ninvoke, SimpleThrottler } from 'vs/base/common/async';
E
Erich Gamma 已提交
11
import { mkdirp, rimraf } from 'vs/base/node/pfs';
12
import { TPromise } from 'vs/base/common/winjs.base';
J
Joao Moreno 已提交
13
import { open as _openZip, Entry, ZipFile } from 'yauzl';
S
Sandeep Somavarapu 已提交
14
import { ILogService } from 'vs/platform/log/common/log';
E
Erich Gamma 已提交
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

export interface IExtractOptions {
	overwrite?: boolean;

	/**
	 * 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 已提交
30
export type ExtractErrorType = 'CorruptZip' | 'Incomplete';
J
Joao Moreno 已提交
31 32 33 34 35 36 37 38 39 40

export class ExtractError extends Error {

	readonly type: ExtractErrorType;
	readonly cause: Error;

	constructor(type: ExtractErrorType, cause: Error) {
		let message = cause.message;

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

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

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

	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 已提交
58
function toExtractError(err: Error): ExtractError {
S
Sandeep Somavarapu 已提交
59 60 61 62 63
	if (err instanceof ExtractError) {
		return err;
	}

	let type: ExtractErrorType = void 0;
J
Joao Moreno 已提交
64 65

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

	return new ExtractError(type, err);
}

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

80
	let istream: WriteStream;
81
	return mkdirp(targetDirName).then(() => new TPromise((c, e) => {
82
		istream = createWriteStream(targetFileName, { mode });
83
		istream.once('close', () => c(null));
84 85 86
		istream.once('error', e);
		stream.once('error', e);
		stream.pipe(istream);
87 88 89 90
	}, () => {
		if (istream) {
			istream.close();
		}
91
	}));
E
Erich Gamma 已提交
92 93
}

S
Sandeep Somavarapu 已提交
94
function extractZip(zipfile: ZipFile, targetPath: string, options: IOptions, logService: ILogService): TPromise<void> {
95 96
	let isCanceled = false;
	let last = TPromise.wrap<any>(null);
S
Sandeep Somavarapu 已提交
97
	let extractedEntriesCount = 0;
98

99
	return new TPromise((c, e) => {
100
		const throttler = new SimpleThrottler();
E
Erich Gamma 已提交
101

S
Sandeep Somavarapu 已提交
102 103 104 105 106
		const readNextEntry = () => {
			extractedEntriesCount++;
			zipfile.readEntry();
		};

E
Erich Gamma 已提交
107
		zipfile.once('error', e);
S
Sandeep Somavarapu 已提交
108 109 110 111
		zipfile.once('close', () => last.then(() => {
			if (isCanceled || zipfile.entryCount === extractedEntriesCount) {
				c(null);
			} else {
112
				e(new ExtractError('Incomplete', new Error(nls.localize('incompleteExtract', "Incomplete. Found {0} of {1} entries", extractedEntriesCount, zipfile.entryCount))));
S
Sandeep Somavarapu 已提交
113 114
			}
		}, e));
S
Sandeep Somavarapu 已提交
115
		zipfile.readEntry();
E
Erich Gamma 已提交
116
		zipfile.on('entry', (entry: Entry) => {
117

118 119 120 121
			if (isCanceled) {
				return;
			}

E
Erich Gamma 已提交
122
			if (!options.sourcePathRegex.test(entry.fileName)) {
S
Sandeep Somavarapu 已提交
123
				readNextEntry();
E
Erich Gamma 已提交
124 125 126
				return;
			}

127
			const fileName = entry.fileName.replace(options.sourcePathRegex, '');
J
Joao Moreno 已提交
128 129 130 131

			// directory file names end with '/'
			if (/\/$/.test(fileName)) {
				const targetFileName = path.join(targetPath, fileName);
S
Sandeep Somavarapu 已提交
132
				last = mkdirp(targetFileName).then(() => readNextEntry());
J
Joao Moreno 已提交
133 134 135 136
				return;
			}

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

S
Sandeep Somavarapu 已提交
139
			last = throttler.queue(() => stream.then(stream => extractEntry(stream, fileName, mode, targetPath, options).then(() => readNextEntry())));
E
Erich Gamma 已提交
140
		});
141
	}, () => {
142
		logService.debug(targetPath, 'Cancelled.');
143 144 145
		isCanceled = true;
		last.cancel();
		zipfile.close();
J
Joao Moreno 已提交
146 147 148
	}).then(null, err => TPromise.wrapError(toExtractError(err)));
}

S
Sandeep Somavarapu 已提交
149 150
function openZip(zipFile: string, lazy: boolean = false): TPromise<ZipFile> {
	return nfcall<ZipFile>(_openZip, zipFile, lazy ? { lazyEntries: true } : void 0)
J
Joao Moreno 已提交
151
		.then(null, err => TPromise.wrapError(toExtractError(err)));
E
Erich Gamma 已提交
152 153
}

S
Sandeep Somavarapu 已提交
154
export function extract(zipPath: string, targetPath: string, options: IExtractOptions = {}, logService: ILogService): TPromise<void> {
J
Johannes Rieken 已提交
155
	const sourcePathRegex = new RegExp(options.sourcePath ? `^${options.sourcePath}` : '');
E
Erich Gamma 已提交
156

S
Sandeep Somavarapu 已提交
157
	let promise = openZip(zipPath, true);
E
Erich Gamma 已提交
158 159

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

S
Sandeep Somavarapu 已提交
163
	return promise.then(zipfile => extractZip(zipfile, targetPath, { sourcePathRegex }, logService));
E
Erich Gamma 已提交
164 165 166
}

function read(zipPath: string, filePath: string): TPromise<Readable> {
J
Joao Moreno 已提交
167
	return openZip(zipPath).then(zipfile => {
E
Erich Gamma 已提交
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
		return new TPromise<Readable>((c, e) => {
			zipfile.on('entry', (entry: Entry) => {
				if (entry.fileName === filePath) {
					ninvoke<Readable>(zipfile, zipfile.openReadStream, entry).done(stream => c(stream), err => e(err));
				}
			});

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

export function buffer(zipPath: string, filePath: string): TPromise<Buffer> {
	return read(zipPath, filePath).then(stream => {
		return new TPromise<Buffer>((c, e) => {
B
Benjamin Pasero 已提交
183
			const buffers: Buffer[] = [];
E
Erich Gamma 已提交
184
			stream.once('error', e);
J
Joao Moreno 已提交
185
			stream.on('data', b => buffers.push(b as Buffer));
E
Erich Gamma 已提交
186 187 188 189
			stream.on('end', () => c(Buffer.concat(buffers)));
		});
	});
}