291 lines
7 KiB
JavaScript
291 lines
7 KiB
JavaScript
/* --------------------
|
|
* yauzl-promise module
|
|
* Promisify yauzl
|
|
* ------------------*/
|
|
|
|
'use strict';
|
|
|
|
// Modules
|
|
const cloner = require('yauzl-clone');
|
|
|
|
// Constants
|
|
const STATE = Symbol(),
|
|
STORED_ERROR = Symbol();
|
|
|
|
// Exports
|
|
module.exports = (yauzl, Promise) => {
|
|
const {ZipFile, Entry} = yauzl;
|
|
|
|
// Promisify open + from... methods
|
|
promisifyMethod(yauzl, Promise, 'open');
|
|
promisifyMethod(yauzl, Promise, 'fromFd');
|
|
promisifyMethod(yauzl, Promise, 'fromBuffer');
|
|
promisifyMethod(yauzl, Promise, 'fromRandomAccessReader');
|
|
|
|
// Promisify `close` method
|
|
promisifyClose(ZipFile, Promise);
|
|
|
|
// Promisify ZipFile `readEntry` method
|
|
promisifyReadEntry(ZipFile, Promise);
|
|
|
|
// Add ZipFile `readEntries` + `walkEntries` methods
|
|
ZipFile.prototype.readEntries = readEntries;
|
|
addWalkEntriesMethod(ZipFile, Promise);
|
|
|
|
// Promisify ZipFile `openReadStream` method
|
|
promisifyOpenReadStream(ZipFile, Promise);
|
|
|
|
// Add Entry `openReadStream` method
|
|
Entry.prototype.openReadStream = entryOpenReadStream;
|
|
|
|
// Add reference to Entry to ZipFile (used by `readEntries`)
|
|
ZipFile.Entry = Entry;
|
|
};
|
|
|
|
/*
|
|
* Promisify open/from... method
|
|
*/
|
|
function promisifyMethod(yauzl, Promise, fnName) {
|
|
const fromBuffer = fnName == 'fromBuffer';
|
|
|
|
cloner.patch(yauzl, fnName, original => {
|
|
return function(path, totalSize, options) {
|
|
return new Promise((resolve, reject) => {
|
|
options = Object.assign({}, options, {lazyEntries: true, autoClose: false});
|
|
|
|
original(path, totalSize, options, (err, zipFile) => {
|
|
if (err) return reject(err);
|
|
opened(zipFile, resolve, fromBuffer, yauzl);
|
|
});
|
|
});
|
|
};
|
|
});
|
|
}
|
|
|
|
function opened(zipFile, resolve, fromBuffer, yauzl) {
|
|
// For `.fromBuffer()` calls, adapt `reader` to emit close event
|
|
if (fromBuffer) {
|
|
zipFile.reader.unref = yauzl.RandomAccessReader.prototype.unref;
|
|
zipFile.reader.close = cb => cb();
|
|
}
|
|
|
|
// Init
|
|
clearState(zipFile);
|
|
clearError(zipFile);
|
|
|
|
// Intercept events
|
|
zipFile.intercept('entry', emittedEntry);
|
|
zipFile.intercept('end', emittedEnd);
|
|
zipFile.intercept('close', emittedClose);
|
|
zipFile.intercept('error', emittedError);
|
|
|
|
// Resolve promise with zip object
|
|
resolve(zipFile);
|
|
}
|
|
|
|
/*
|
|
* Error event handler
|
|
*/
|
|
function emittedError(err) {
|
|
// jshint validthis:true
|
|
// If operation in progress, reject its promise
|
|
const state = getState(this);
|
|
if (state) {
|
|
clearState(this);
|
|
return state.reject(err);
|
|
}
|
|
|
|
// Store error to be returned on next call to
|
|
// `.readEntry()`, `.close()` or `.openReadStream()`.
|
|
if (!getError(this)) setError(this, err);
|
|
}
|
|
|
|
function rejectWithStoredError(zipFile, reject) {
|
|
const err = getError(zipFile);
|
|
clearError(zipFile);
|
|
reject(err);
|
|
}
|
|
|
|
/*
|
|
* Promisify ZipFile `close` method
|
|
*/
|
|
function promisifyClose(ZipFile, Promise) {
|
|
const close = ZipFile.prototype.close;
|
|
|
|
ZipFile.prototype.close = function() {
|
|
return new Promise((resolve, reject) => {
|
|
if (getError(this)) return rejectWithStoredError(this, reject);
|
|
if (!this.isOpen) return resolve();
|
|
if (getState(this)) return reject(new Error('Previous operation has not completed yet'));
|
|
|
|
setState(this, {action: 'close', resolve, reject});
|
|
close.call(this);
|
|
});
|
|
};
|
|
}
|
|
|
|
function emittedClose() {
|
|
// jshint validthis:true
|
|
// If not closing, emit error
|
|
const state = getState(this);
|
|
if (!state || state.action != 'close') return this.emit('error', new Error('Unexpected \'close\' event emitted'));
|
|
|
|
clearState(this);
|
|
|
|
// Resolve promise
|
|
state.resolve();
|
|
}
|
|
|
|
/*
|
|
* Promisify ZipFile `readEntry` method
|
|
*/
|
|
function promisifyReadEntry(ZipFile, Promise) {
|
|
const readEntry = ZipFile.prototype.readEntry;
|
|
|
|
ZipFile.prototype.readEntry = function() {
|
|
return new Promise((resolve, reject) => {
|
|
if (getError(this)) return rejectWithStoredError(this, reject);
|
|
if (!this.isOpen) return reject(new Error('ZipFile is not open'));
|
|
if (getState(this)) return reject(new Error('Previous operation has not completed yet'));
|
|
|
|
setState(this, {action: 'read', resolve, reject});
|
|
readEntry.call(this);
|
|
});
|
|
};
|
|
}
|
|
|
|
function emittedEntry(entry) {
|
|
// jshint validthis:true
|
|
// If not reading, emit error
|
|
const state = getState(this);
|
|
if (!state || state.action != 'read') return this.emit('error', new Error(`Unexpected '${entry ? 'entry' : 'end'}' event emitted`));
|
|
|
|
clearState(this);
|
|
|
|
// Set reference to zipFile on entry (used by `entry.openReadStream()`)
|
|
if (entry) entry.zipFile = this;
|
|
|
|
// Resolve promise with entry
|
|
state.resolve(entry);
|
|
}
|
|
|
|
function emittedEnd() {
|
|
// jshint validthis:true
|
|
emittedEntry.call(this, null);
|
|
}
|
|
|
|
/*
|
|
* Functions to access state
|
|
*/
|
|
function getState(zipFile) {
|
|
return zipFile[STATE];
|
|
}
|
|
|
|
function setState(zipFile, state) {
|
|
zipFile[STATE] = state;
|
|
}
|
|
|
|
function clearState(zipFile) {
|
|
zipFile[STATE] = undefined;
|
|
}
|
|
|
|
function getError(zipFile) {
|
|
return zipFile[STORED_ERROR];
|
|
}
|
|
|
|
function setError(zipFile, state) {
|
|
zipFile[STORED_ERROR] = state;
|
|
}
|
|
|
|
function clearError(zipFile) {
|
|
zipFile[STORED_ERROR] = undefined;
|
|
}
|
|
|
|
/*
|
|
* Read all ZipFile entries
|
|
* Reads all entries and returns a promise which resolves with an array of entries
|
|
* `options.max` limits number returned (default 100)
|
|
* `options.max` can be set to `0` for no limit
|
|
*/
|
|
function readEntries(numEntries) {
|
|
// jshint validthis:true
|
|
const entries = [];
|
|
return this.walkEntries(entry => {
|
|
entries.push(entry);
|
|
}, numEntries).then(() => {
|
|
return entries;
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Walk all ZipFile entries
|
|
* Walks through each entry and calls `fn` with each.
|
|
* Returns a promise which resolves when all have been read.
|
|
*/
|
|
function addWalkEntriesMethod(ZipFile, Promise) {
|
|
ZipFile.prototype.walkEntries = function(callback, numEntries) {
|
|
callback = wrapFunctionToReturnPromise(callback, Promise);
|
|
|
|
return new Promise((resolve, reject) => {
|
|
walkNextEntry(this, callback, numEntries, 0, err => {
|
|
if (err) return reject(err);
|
|
resolve();
|
|
});
|
|
});
|
|
};
|
|
}
|
|
|
|
function walkNextEntry(zipFile, fn, numEntries, count, cb) {
|
|
if (numEntries && count == numEntries) return cb();
|
|
|
|
zipFile.readEntry().then(entry => {
|
|
if (!entry) return cb();
|
|
|
|
return fn(entry).then(() => {
|
|
walkNextEntry(zipFile, fn, numEntries, count + 1, cb);
|
|
});
|
|
}).catch(err => {
|
|
cb(err);
|
|
});
|
|
}
|
|
|
|
/*
|
|
* Promisify ZipFile `openReadStream` method
|
|
*/
|
|
function promisifyOpenReadStream(ZipFile, Promise) {
|
|
const openReadStream = ZipFile.prototype.openReadStream;
|
|
ZipFile.prototype.openReadStream = function(entry, options) {
|
|
return new Promise((resolve, reject) => {
|
|
if (getError(this)) return rejectWithStoredError(this, reject);
|
|
openReadStream.call(this, entry, options || {}, (err, stream) => {
|
|
if (err) return reject(err);
|
|
resolve(stream);
|
|
});
|
|
});
|
|
};
|
|
}
|
|
|
|
/*
|
|
* Entry `openReadStream` method
|
|
*/
|
|
function entryOpenReadStream(options) {
|
|
// jshint validthis:true
|
|
return this.zipFile.openReadStream(this, options);
|
|
}
|
|
|
|
/*
|
|
* Utility functions
|
|
*/
|
|
function wrapFunctionToReturnPromise(fn, Promise) {
|
|
return function() {
|
|
try {
|
|
const result = fn.apply(this, arguments);
|
|
if (result instanceof Promise) return result;
|
|
return Promise.resolve(result);
|
|
} catch (err) {
|
|
return new Promise((resolve, reject) => { // jshint ignore:line
|
|
reject(err);
|
|
});
|
|
}
|
|
};
|
|
}
|