onezip
Version:
pack and extract .zip archives with emitter
306 lines (227 loc) • 7.29 kB
JavaScript
'use strict';
const {
createReadStream,
createWriteStream,
} = require('fs');
const {
mkdir,
stat,
unlink,
} = require('fs/promises');
const path = require('path');
const {EventEmitter} = require('events');
const {inherits, promisify} = require('util');
const assert = require('assert');
const pipe = require('pipe-io');
const tryToCatch = require('try-to-catch');
const yazl = require('yazl');
const yauzl = require('yauzl');
const superfind = require('./superfind');
const {dirname} = path;
inherits(OneZip, EventEmitter);
module.exports = onezip;
module.exports.pack = onezip('pack');
module.exports.extract = onezip('extract');
function check(from, to, files) {
assert(typeof from === 'string', 'from should be a string!');
assert(/string|object/.test(typeof to), 'to should be string or object!');
if (arguments.length > 2)
assert(Array.isArray(files), 'files should be an array!');
}
function checkOperation(operation) {
if (!/^(pack|extract)$/.test(operation))
throw Error('operations could be "pack" or "extract" only!');
}
function onezip(operation) {
checkOperation(operation);
return (from, to, files) => {
return new OneZip(operation, from, to, files);
};
}
function OneZip(operation, from, to, files) {
if (operation === 'extract')
check(from, to);
else
check(from, to, files);
process.nextTick(async () => {
EventEmitter.call(this);
this._i = 0;
this._n = 0;
this._percent = 0;
this._percentPrev = 0;
this._names = [];
if (operation === 'pack') {
this._from = endSlash(from);
this._to = to;
if (!files.length)
return this.emit('error', Error('Nothing to pack!'));
await this._parallel(from, files);
if (this._abort)
return this.emit('end');
this._pack();
return;
}
this._from = from;
this._to = endSlash(to);
const [error] = await tryToCatch(this._parse.bind(this), from);
if (error)
return this.emit('error', error);
this._extract(from);
});
}
OneZip.prototype.abort = function() {
this._abort = true;
};
OneZip.prototype._parallel = async function(from, files) {
const promises = [];
for (const name of files) {
const full = path.join(from, name);
promises.push(this._findFiles(full));
}
const all = Promise.all.bind(Promise, promises);
const [error] = await tryToCatch(all);
if (error) {
this.emit('error', error);
this.abort();
}
};
OneZip.prototype._findFiles = async function(filename) {
const {names} = await superfind(filename);
this._n = names.length;
this._names = names;
};
OneZip.prototype._pack = async function() {
this.emit('start');
const {
_to,
_from,
_names,
} = this;
const zipfile = new yazl.ZipFile();
const end = (name) => {
this.emit('file', name);
this._progress();
};
for (const _name of _names) {
const filename = _name.replace(_from, '');
const [error, data] = await tryToCatch(stat, _name);
if (error)
return this.emit('error', error);
if (data.isDirectory()) {
zipfile.addEmptyDirectory(filename);
end(_name);
continue;
}
const stream = this._createReadStream(_name, () => {
end(_name);
});
zipfile.addReadStream(stream, filename);
}
zipfile.end();
const streamFile = typeof _to === 'object' ?
_to : createWriteStream(_to);
const [errorPipe] = await tryToCatch(pipe, [
zipfile.outputStream,
streamFile,
]);
if (errorPipe)
return this.emit('error', errorPipe);
if (!this._abort)
return this.emit('end');
await this._unlink(_to);
};
OneZip.prototype._createReadStream = function(filename, end) {
return createReadStream(filename)
.on('error', (error) => {
this.emit('error', error);
})
.on('end', end);
};
OneZip.prototype._onOpenReadStream = function(success) {
return (error, readStream = {}) => {
if (error)
return this.emit('error', error);
success(readStream);
};
};
OneZip.prototype._unlink = async function(to) {
const [error] = await tryToCatch(unlink, to);
if (error)
return this.emit('error', error);
this.emit('end');
};
OneZip.prototype._parse = promisify(function(from, fn) {
yauzl.open(from, (error, zipfile) => {
if (error)
return fn(error);
zipfile.on('entry', () => {
++this._n;
});
zipfile.once('end', fn);
});
});
OneZip.prototype._extract = function(from) {
this.emit('start');
const lazyEntries = true;
const autoClose = true;
const options = {
lazyEntries,
autoClose,
};
yauzl.open(from, options, (error, zipfile) => {
const handleError = (error) => {
this.emit('error', error);
};
if (error)
return handleError(error);
zipfile.readEntry();
zipfile.on('entry', async (entry) => {
const {fileName} = entry;
const fn = (error) => {
if (error)
return handleError(error);
this._progress();
this.emit('file', fileName);
zipfile.readEntry();
};
const name = path.join(this._to, fileName);
if (/\/$/.test(fileName)) {
const [e] = await tryToCatch(mkdir, name, {
recursive: true,
});
return fn(e);
}
zipfile.openReadStream(entry, this._onOpenReadStream(async (readStream) => {
let e;
[e] = await tryToCatch(mkdir, dirname(name), {
recursive: true,
});
if (e)
return fn(e);
[e] = await tryToCatch(pipe, [
readStream,
createWriteStream(name),
]);
fn(e);
}));
});
zipfile.once('end', () => {
this.emit('end');
});
});
};
OneZip.prototype._progress = function() {
++this._i;
const value = Math.round(this._i * 100 / this._n);
this._percent = value;
if (value !== this._percentPrev) {
this._percentPrev = value;
this.emit('progress', value);
}
};
function endSlash(str) {
const last = str.length - 1;
if (str[last] === path.sep)
return str;
return str + path.sep;
}