UNPKG

jspurefix

Version:
204 lines (189 loc) 7.21 kB
const yauzl = require('yauzl') const path = require('path') const fs = require('fs') const util = require('util') const Transform = require('stream').Transform // thanks to https://github.com/thejoshwolfe/yauzl/blob/master/examples/unzip.js let zipFilePath = null let offsetArg let lenArg let endArg const args = process.argv.slice(2) for (let i = 0; i < args.length; i++) { const arg = args[i] if (arg === '--offset') { i += 1 offsetArg = parseInt(args[i]) if (isNaN(offsetArg)) throw new Error('--offset argument not parsable as an int') } else if (arg === '--len') { i += 1 lenArg = parseInt(args[i]) if (isNaN(lenArg)) throw new Error('--len argument not parsable as an int') } else if (arg === '--end') { i += 1 endArg = parseInt(args[i]) if (isNaN(endArg)) throw new Error('--end argument not parsable as an int') } else if (['-h', '--help'].includes(arg)) { // print help zipFilePath = null break } else if (/^--/.test(arg)) { throw new Error('unrecognized option: ' + arg) } else { if (zipFilePath != null) throw new Error('too many arguments') zipFilePath = arg } } if (zipFilePath == null || /^-/.test(zipFilePath) || (lenArg != null && endArg != null)) { console.log( 'usage: node unzip.js [options] path/to/file.zip\n' + '\n' + 'unzips the specified zip file into the current directory\n' + '\n' + 'options:\n' + ' --offset START\n' + ' --len LEN\n' + ' --end END\n' + ' interprets the middle of the specified file as a zipfile.\n' + ' starting START number of bytes in from the beginning (default 0).\n' + ' end with length of LEN (default is all the way to the end of the file).\n' + ' or end at byte offset END (exclusive) (default is the end of the file).\n' + ' end can be negative to count backwards from the end of the file\n' + ' (example, `--end -1` excludes the last byte of the file).\n' + '') process.exit(1) } function mkdirp (dir, cb) { if (dir === '.') return cb() fs.stat(dir, function (err) { if (err == null) return cb() // already exists const parent = path.dirname(dir) mkdirp(parent, function () { process.stdout.write(dir.replace(/\/$/, '') + '/\n') fs.mkdir(dir, cb) }) }) } if (offsetArg != null || lenArg != null || endArg != null) { openMiddleOfFile(zipFilePath, { lazyEntries: true }, offsetArg, lenArg, endArg, handleZipFile) } else { yauzl.open(zipFilePath, { lazyEntries: true }, handleZipFile) } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type function openMiddleOfFile (zipFilePath, options, offsetArg, lenArg, endArg, handleZipFile) { fs.open(zipFilePath, 'r', function (err, fd) { if (err != null) throw err fs.fstat(fd, function (err, stats) { if (err) { console.log(err) } // resolve optional parameters if (offsetArg == null) offsetArg = 0 if (lenArg == null && endArg == null) endArg = stats.size if (endArg == null) endArg = lenArg + offsetArg else if (endArg < 0) endArg = stats.size + endArg // validate parameters if (offsetArg < 0) throw new Error('--offset < 0') if (lenArg < 0) throw new Error('--len < 0') if (offsetArg > endArg) throw new Error('--offset > --end') if (endArg > stats.size) throw new Error('--end/--len goes past EOF') // extend RandomAccessReader function MiddleOfFileReader () { yauzl.RandomAccessReader.call(this) } util.inherits(MiddleOfFileReader, yauzl.RandomAccessReader) // implement required and option methods MiddleOfFileReader.prototype._readStreamForRange = function (start, end) { return fs.createReadStream(null, { fd, // shift the start and end offsets start: start + offsetArg, end: end + offsetArg - 1, // the -1 is because fs.createReadStream()'s end option is inclusive autoClose: false }) } MiddleOfFileReader.prototype.read = function (buffer, offset, length, position, callback) { // shift the position fs.read(fd, buffer, offset, length, position + offsetArg, callback) } MiddleOfFileReader.prototype.close = function (callback) { fs.close(fd, callback) } yauzl.fromRandomAccessReader(new MiddleOfFileReader(), endArg - offsetArg, options, handleZipFile) }) }) } function handleZipFile (err, zipfile) { if (err) throw err // track when we've closed all our file handles let handleCount = 0 function incrementHandleCount () { handleCount++ } function decrementHandleCount () { handleCount-- if (handleCount === 0) { console.log('all input and output handles closed') } } incrementHandleCount() zipfile.on('close', function () { console.log('closed input file') decrementHandleCount() }) zipfile.readEntry() zipfile.on('entry', function (entry) { if (/\/$/.test(entry.fileName)) { // directory file names end with '/' mkdirp(entry.fileName, function () { if (err) throw err zipfile.readEntry() }) } else { // ensure parent directory exists mkdirp(path.dirname(entry.fileName), function () { zipfile.openReadStream(entry, function (err, readStream) { if (err) throw err // report progress through large files let byteCount = 0 const totalBytes = entry.uncompressedSize let lastReportedString = byteCount + '/' + totalBytes + ' 0%' process.stdout.write(entry.fileName + '...' + lastReportedString) function reportString (msg) { let clearString = '' for (let i = 0; i < lastReportedString.length; i++) { clearString += '\b' if (i >= msg.length) { clearString += ' \b' } } process.stdout.write(clearString + msg) lastReportedString = msg } // report progress at 60Hz const progressInterval = setInterval(function () { reportString(byteCount + '/' + totalBytes + ' ' + ((byteCount / totalBytes * 100) | 0) + '%') }, 1000 / 60) const filter = new Transform() filter._transform = function (chunk, encoding, cb) { byteCount += chunk.length cb(null, chunk) } filter._flush = function (cb) { clearInterval(progressInterval) reportString('') // delete the "..." process.stdout.write('\b \b\b \b\b \b\n') cb() zipfile.readEntry() } // pump file contents const writeStream = fs.createWriteStream(entry.fileName) incrementHandleCount() writeStream.on('close', decrementHandleCount) readStream.pipe(filter).pipe(writeStream) }) }) } }) }