yauzl-promise
Version:
Unzip library for NodeJS
290 lines (254 loc) • 7.51 kB
JavaScript
/* --------------------
* yauzl-promise module
* Reader classes
* ------------------*/
;
// Modules
const fs = require('fs'),
{PassThrough: PassThroughStream, Readable: ReadableStream} = require('stream'),
{promisify} = require('util'),
{isPositiveIntegerOrZero} = require('is-it-type'),
assert = require('simple-invariant');
const openAsync = promisify(fs.open),
closeAsync = promisify(fs.close);
// Imports
const {streamToBuffer} = require('./utils.js');
// Exports
/**
* `Reader` class.
* `FileReader`, `FdReader` and `BufferReader` subclass this.
*
* Users can create custom `Reader`s by subclassing and implementing the following methods:
* - `_createReadStream(start, length)` (required)
* - `_read(start, length)` (optional)
* - `_open()` (optional)
* - `_close()` (optional)
*/
class Reader {
constructor() {
this.isOpen = false;
this.readCount = 0;
}
/**
* Open reader.
* Calls `._open()` method defined by subclass.
* If already open, does nothing.
* @async
* @returns {undefined}
*/
async open() {
if (this.isOpen) return;
this.isOpen = true;
await this._open();
}
/**
* Close reader.
* Calls `._close()` method defined by subclass.
* If already closed, does nothing.
* @async
* @returns {undefined}
* @throws {Error} - If Reader is currently being read from
*/
async close() {
if (!this.isOpen) return;
assert(this.readCount === 0, 'Cannot close while reading in progress');
this.isOpen = false;
await this._close();
}
/**
* Read bytes into Buffer.
* @async
* @param {number} start - Starting position to read at
* @param {number} length - Number of bytes to read
* @returns {Buffer} - Buffer
* @throws {Error} - If Reader is not open
*/
async read(start, length) {
// Don't validate `start` + `length` because this is called so often
assert(this.isOpen, 'Cannot call `read()` on a reader which is not open');
if (length === 0) return Buffer.allocUnsafe();
this.readCount++;
try {
return await this._read(start, length);
} finally {
this.readCount--;
}
}
/**
* Create readable stream to read from Reader.
* @param {number} start - Position to start reading at
* @param {number} length - Number of bytes to read
* @returns {Object} - Readable stream
* @throws {Error} - If arguments invalid or reader is not open
*/
createReadStream(start, length) {
// Validate input
assert(isPositiveIntegerOrZero(start), '`start` must be a positive integer or zero');
assert(isPositiveIntegerOrZero(length), '`length` must be a positive integer or zero');
// Error if not open
assert(this.isOpen, 'Cannot call `createReadStream()` on a reader which is not open');
// Return empty stream for zero-size request
if (length === 0) {
const emptyStream = new PassThroughStream();
setImmediate(() => emptyStream.end());
return emptyStream;
}
// Get stream
this.readCount++;
try {
const stream = this._createReadStream(start, length);
// Mark stream as ended on an `end`, `error` or `close` event.
// In Node v16, these events don't reliably fire if stream is destroyed with `.destroy()`
// so capture that too.
let isEnded = false;
const onEnd = () => {
if (isEnded) return;
isEnded = true;
this.readCount--;
};
const originalDestroy = stream.destroy;
stream.destroy = function(err) {
onEnd();
return originalDestroy.call(this, err);
};
stream.on('end', onEnd);
stream.on('error', onEnd);
stream.on('close', onEnd);
return stream;
} catch (err) {
this.readCount--;
throw err;
}
}
/**
* Open Reader.
* Default implementation does nothing. Subclasses can optionally implement this.
* @async
* @returns {undefined}
*/
async _open() {} // eslint-disable-line class-methods-use-this, no-empty-function
/**
* Close Reader.
* Default implementation does nothing. Subclasses can optionally implement this.
* @async
* @returns {undefined}
*/
async _close() {} // eslint-disable-line class-methods-use-this, no-empty-function
/**
* Read bytes from Reader into a Buffer.
* Subclasses can override this.
* @async
* @param {number} start - Starting position to read at
* @param {number} length - Number of bytes to read
* @returns {Buffer} - Buffer
*/
async _read(start, length) {
const stream = this._createReadStream(start, length);
const buffer = await streamToBuffer(stream);
assert(buffer.length === length, 'Unexpected end of file');
return buffer;
}
// eslint-disable-next-line jsdoc/require-returns-check
/**
* Create readable stream to read from Reader.
* Subclasses must implement this.
* @param {number} start - Position to start reading at
* @param {number} length - Number of bytes to read
* @returns {Object} - Readable stream
* @throws {Error} - If fail to create stream
*/
_createReadStream(start, length) { // eslint-disable-line class-methods-use-this, no-unused-vars
throw new Error('Not implemented');
}
}
// Shim of `fs` module to prevent file descriptor being closed if stream is destroyed
const shimmedFs = {
open() {
throw new Error(
'Shimmed FS `open` method should not be called. If you get this error, please raise an issue.'
);
},
read(...args) {
return fs.read(...args);
},
close(fd, cb) {
setImmediate(() => cb(null));
}
};
class FdReader extends Reader {
/**
* Create `FdReader`.
* @param {number} fd - File descriptor
*/
constructor(fd) {
super();
this.fd = fd;
}
_close() {
return closeAsync(this.fd);
}
_read(start, length) {
return new Promise((resolve, reject) => {
const buffer = Buffer.allocUnsafe(length);
fs.read(this.fd, buffer, 0, length, start, (err, bytesRead) => {
if (err) {
reject(err);
} else if (bytesRead !== length) {
reject(new Error('Unexpected end of file'));
} else {
resolve(buffer);
}
});
});
}
_createReadStream(start, length) {
// Use shimmed `fs` with inactive `close()` method,
// to prevent file descriptor getting closed when stream ends.
// `autoClose` option works for this purpose when stream ends naturally,
// but FD still gets closed if `.destroy()` is called.
// Shimming FS is only way I around this that I could find.
return fs.createReadStream(null, {start, end: start + length - 1, fd: this.fd, fs: shimmedFs});
}
}
class FileReader extends Reader {
/**
* Create `FileReader`.
* @param {string} path - File path
*/
constructor(path) {
super();
this.path = path;
this.fd = null;
}
async _open() {
this.fd = await openAsync(this.path, 'r', 0o444);
}
async _close() {
await closeAsync(this.fd);
this.fd = null;
}
}
FileReader.prototype._read = FdReader.prototype._read;
FileReader.prototype._createReadStream = FdReader.prototype._createReadStream;
class BufferReader extends Reader {
/**
* Create `BufferReader`.
* @param {Buffer} buffer - Buffer
*/
constructor(buffer) {
super();
this.buffer = buffer;
}
async _read(start, length) {
const end = start + length;
assert(end <= this.buffer.length, 'Cannot read beyond end of buffer');
return this.buffer.subarray(start, end);
}
_createReadStream(start, length) {
const end = start + length;
assert(end <= this.buffer.length, 'Cannot read beyond end of buffer');
const slice = this.buffer.subarray(start, end);
return ReadableStream.from(slice);
}
}
module.exports = {Reader, FdReader, FileReader, BufferReader};