zip-lib
Version:
zip and unzip library for node
385 lines (384 loc) • 13.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Unzip = void 0;
const yauzl = require("yauzl");
const exfs = require("./fs");
const fs_1 = require("fs");
const fs = require("fs/promises");
const path = require("path");
const cancelable_1 = require("./cancelable");
class EntryEvent {
/**
*
*/
constructor(_entryCount) {
this._entryCount = _entryCount;
this._isPrevented = false;
}
get entryName() {
return this._entryName;
}
set entryName(name) {
this._entryName = name;
}
get entryCount() {
return this._entryCount;
}
get isPrevented() {
return this._isPrevented;
}
preventDefault() {
this._isPrevented = true;
}
reset() {
this._isPrevented = false;
}
}
class EntryContext {
constructor(_targetFolder, _realTargetFolder, symlinkAsFileOnWindows) {
this._targetFolder = _targetFolder;
this._realTargetFolder = _realTargetFolder;
this.symlinkAsFileOnWindows = symlinkAsFileOnWindows;
this._symlinkFileNames = [];
this._symlinkFolders = [];
this._ensuredFolders = [];
}
get decodeEntryFileName() {
return this._decodeEntryFileName;
}
set decodeEntryFileName(name) {
this._decodeEntryFileName = name;
}
get targetFolder() {
return this._targetFolder;
}
get realTargetFolder() {
return this._realTargetFolder;
}
get symlinkFileNames() {
return this._symlinkFileNames;
}
addSymlinkFolder(folder, realpath) {
const find = this._symlinkFolders.find((item) => item.folder === folder);
if (!find) {
this._symlinkFolders.push({ folder, realpath });
}
}
getFilePath() {
return path.join(this.targetFolder, this.decodeEntryFileName);
}
async ensureFolder(folder) {
if (this._ensuredFolders.includes(folder)) {
return;
}
const folderStat = await exfs.ensureFolder(folder);
if (folderStat.isSymbolicLink) {
this.addSymlinkFolder(folder, folderStat.realpath);
}
this._ensuredFolders.push(folder);
}
async isOutsideTargetFolder(tpath) {
if (this._symlinkFileNames.length === 0 &&
this._symlinkFolders.length === 0) {
return false;
}
if (process.platform === "win32" &&
this.symlinkAsFileOnWindows) {
return false;
}
for (const { folder, realpath } of this._symlinkFolders) {
if (tpath.includes(folder)) {
if (realpath.indexOf(this.realTargetFolder) !== 0) {
return true;
}
}
}
for (const fileName of this._symlinkFileNames) {
if (tpath.includes(fileName)) {
const realFilePath = await exfs.realpath(tpath);
if (realFilePath.indexOf(this.realTargetFolder) !== 0) {
return true;
}
}
}
return false;
}
}
/**
* Extract the zip file.
*/
class Unzip extends cancelable_1.Cancelable {
/**
*
*/
constructor(options) {
super();
this.options = options;
}
/**
* Extract the zip file to the specified location.
* @param zipFile
* @param targetFolder
* @param options
*/
async extract(zipFile, targetFolder) {
let extractedEntriesCount = 0;
const token = new cancelable_1.CancellationToken();
this.token = token;
if (this.isOverwrite()) {
await exfs.rimraf(targetFolder);
}
if (token.isCancelled) {
return Promise.reject(this.canceledError());
}
await exfs.ensureFolder(targetFolder);
const realTargetFolder = await exfs.realpath(targetFolder);
const zfile = await this.openZip(zipFile, token);
this.zipFile = zfile;
zfile.readEntry();
return new Promise((c, e) => {
let anyError = null;
const total = zfile.entryCount;
zfile.once("error", (err) => {
this.closeZip();
e(this.wrapError(err, token.isCancelled));
});
zfile.once("close", () => {
this.zipFile = null;
if (anyError) {
e(this.wrapError(anyError, token.isCancelled));
}
else {
if (token.isCancelled) {
e(this.canceledError());
}
// If the zip content is empty, it will not receive the `zfile.on("entry")` event.
else if (total === 0) {
c(void 0);
}
}
});
// Because openZip is an asynchronous method, openZip may not be completed when calling cancel,
// so we need to check if it has been canceled after the openZip method returns.
if (token.isCancelled) {
this.closeZip();
return;
}
const entryContext = new EntryContext(targetFolder, realTargetFolder, this.symlinkToFile());
const entryEvent = new EntryEvent(total);
zfile.on("entry", async (entry) => {
// use UTF-8 in all situations
// see https://github.com/thejoshwolfe/yauzl/issues/84
const rawName = entry.fileName.toString("utf8");
// allow backslash
const fileName = rawName.replace(/\\/g, "/");
// Because `decodeStrings` is `false`, we need to manually verify the entryname
// see https://github.com/thejoshwolfe/yauzl#validatefilenamefilename
const errorMessage = yauzl.validateFileName(fileName);
if (errorMessage != null) {
anyError = new Error(errorMessage);
this.closeZip();
e(anyError);
return;
}
entryEvent.entryName = fileName;
this.onEntryCallback(entryEvent);
entryContext.decodeEntryFileName = fileName;
try {
if (entryEvent.isPrevented) {
entryEvent.reset();
zfile.readEntry();
}
else {
await this.handleEntry(zfile, entry, entryContext, token);
}
extractedEntriesCount++;
if (extractedEntriesCount === total) {
c();
}
}
catch (error) {
anyError = this.wrapError(error, token.isCancelled);
this.closeZip();
e(anyError);
}
});
});
}
/**
* Cancel decompression.
* If the cancel method is called after the extract is complete, nothing will happen.
*/
cancel() {
if (this.token) {
this.token.cancel();
this.token = null;
}
this.closeZip();
}
closeZip() {
if (this.zipFile) {
this.zipFile.close();
this.zipFile = null;
}
}
openZip(zipFile, token) {
return new Promise((c, e) => {
yauzl.open(zipFile, {
lazyEntries: true,
// see https://github.com/thejoshwolfe/yauzl/issues/84
decodeStrings: false
}, (err, zfile) => {
if (err) {
e(this.wrapError(err, token.isCancelled));
}
else {
c(zfile);
}
});
});
}
async handleEntry(zfile, entry, entryContext, token) {
if (/\/$/.test(entryContext.decodeEntryFileName)) {
// Directory file names end with '/'.
// Note that entires for directories themselves are optional.
// An entry's fileName implicitly requires its parent directories to exist.
await exfs.ensureFolder(entryContext.getFilePath());
zfile.readEntry();
}
else {
// file entry
await this.extractEntry(zfile, entry, entryContext, token);
}
}
openZipFileStream(zfile, entry, token) {
return new Promise((c, e) => {
zfile.openReadStream(entry, (err, readStream) => {
if (err) {
e(this.wrapError(err, token.isCancelled));
}
else {
c(readStream);
}
});
});
}
async extractEntry(zfile, entry, entryContext, token) {
const filePath = entryContext.getFilePath();
const fileDir = path.dirname(filePath);
await entryContext.ensureFolder(fileDir);
const outside = await entryContext.isOutsideTargetFolder(fileDir);
if (outside) {
const error = new Error(`Refuse to write file outside "${entryContext.targetFolder}", file: "${filePath}"`);
error.name = "AFWRITE";
return Promise.reject(error);
}
const readStream = await this.openZipFileStream(zfile, entry, token);
await this.writeEntryToFile(readStream, entry, entryContext, token);
zfile.readEntry();
}
async writeEntryToFile(readStream, entry, entryContext, token) {
let fileStream;
token.onCancelled(() => {
if (fileStream) {
readStream.unpipe(fileStream);
fileStream.destroy(this.canceledError());
}
});
return new Promise(async (c, e) => {
try {
const filePath = entryContext.getFilePath();
const mode = this.modeFromEntry(entry);
// see https://unix.stackexchange.com/questions/193465/what-file-mode-is-a-symlink
const isSymlink = ((mode & 0o170000) === 0o120000);
readStream.once("error", (err) => {
e(this.wrapError(err, token.isCancelled));
});
if (isSymlink) {
entryContext.symlinkFileNames.push(entryContext.decodeEntryFileName);
}
if (isSymlink && !this.symlinkToFile()) {
let linkContent = "";
readStream.on("data", (chunk) => {
if (chunk instanceof String) {
linkContent += chunk;
}
else {
linkContent += chunk.toString();
}
});
readStream.once("end", () => {
this.createSymlink(linkContent, filePath).then(c, e);
});
}
else {
fileStream = (0, fs_1.createWriteStream)(filePath, { mode });
fileStream.once("close", () => c());
fileStream.once("error", (err) => {
e(this.wrapError(err, token.isCancelled));
});
readStream.pipe(fileStream);
}
}
catch (error) {
e(this.wrapError(error, token.isCancelled));
}
});
}
modeFromEntry(entry) {
const attr = entry.externalFileAttributes >> 16 || 33188;
return [448 /* S_IRWXU */, 56 /* S_IRWXG */, 7 /* S_IRWXO */]
.map(mask => attr & mask)
.reduce((a, b) => a + b, attr & 61440 /* S_IFMT */);
}
async createSymlink(linkContent, des) {
let linkType = "file";
if (process.platform === 'win32') {
if (/\/$/.test(linkContent)) {
linkType = "dir";
}
else {
let targetPath = linkContent;
if (!path.isAbsolute(linkContent)) {
targetPath = path.join(path.dirname(des), linkContent);
}
try {
const stat = await fs.stat(targetPath);
if (stat.isDirectory()) {
linkType = "dir";
}
}
catch (error) {
// ignore
}
}
}
await fs.symlink(linkContent, des, linkType);
}
isOverwrite() {
if (this.options &&
this.options.overwrite) {
return true;
}
return false;
}
onEntryCallback(event) {
if (this.options && this.options.onEntry) {
this.options.onEntry(event);
}
}
symlinkToFile() {
let symlinkToFile = false;
if (process.platform === "win32") {
if (this.options &&
this.options.symlinkAsFileOnWindows === false) {
symlinkToFile = false;
}
else {
symlinkToFile = true;
}
}
return symlinkToFile;
}
}
exports.Unzip = Unzip;