UNPKG

static-fs

Version:

A static filesystem to bundle files and read them using NodeJS

213 lines (164 loc) 6.55 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReadStream = ReadStream; var _stream = require("stream"); var _util = _interopRequireDefault(require("util")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } // NOTE: this is a raw re-implementation of https://github.com/nodejs/node/blob/v10.x/lib/internal/fs/streams.js#L45 // that is setup to follow our needs for the static filesystem const ERR_STR_OPTS = typeOfOptions => `Expected options to be either an object or a string, but got ${typeOfOptions} instead`; function assertEncoding(encoding) { if (encoding && !Buffer.isEncoding(encoding)) throw new Error(`ERR_INVALID_OPT_VALUE_ENCODING ${encoding}`); } function getOptions(options, defaultOptions) { if (options === null || options === undefined || typeof options === 'function') { return defaultOptions; } if (typeof options === 'string') { defaultOptions = _util.default._extend({}, defaultOptions); defaultOptions.encoding = options; options = defaultOptions; } else if (typeof options !== 'object') { throw new ERR_STR_OPTS(typeof options); } if (options.encoding !== 'buffer') assertEncoding(options.encoding); return options; } function copyObject(source) { const target = {}; for (const key in source) target[key] = source[key]; return target; } const kMinPoolSpace = 128; let pool; // It can happen that we expect to read a large chunk of data, and reserve // a large chunk of the pool accordingly, but the read() call only filled // a portion of it. If a concurrently executing read() then uses the same pool, // the "reserved" portion cannot be used, so we allow it to be re-used as a // new pool later. const poolFragments = []; function allocNewPool(poolSize) { if (poolFragments.length > 0) pool = poolFragments.pop();else pool = Buffer.allocUnsafe(poolSize); pool.used = 0; } function ReadStream(sfs, path, options) { if (!(this instanceof ReadStream)) return new ReadStream(sfs, path, options); this._sfs = sfs; // a little bit bigger buffer and water marks by default options = copyObject(getOptions(options, {})); if (options.highWaterMark === undefined) options.highWaterMark = 64 * 1024; // for backwards compat do not emit close on destroy. options.emitClose = false; _stream.Readable.call(this, options); this.path = path; this.fd = options.fd === undefined ? null : options.fd; this.flags = options.flags === undefined ? 'r' : options.flags; this.mode = options.mode === undefined ? 0o666 : options.mode; this.start = options.start; this.end = options.end; this.autoClose = options.autoClose === undefined ? true : options.autoClose; this.pos = undefined; this.bytesRead = 0; this.closed = false; if (this.start !== undefined) { if (typeof this.start !== 'number' || Number.isNaN(this.start)) { throw new TypeError('"start" option must be a Number'); } if (this.end === undefined) { this.end = Infinity; } else if (typeof this.end !== 'number' || Number.isNaN(this.end)) { throw new TypeError('"end" option must be a Number'); } if (this.start > this.end) { throw new Error('"start" option must be <= "end" option'); } this.pos = this.start; } if (typeof this.end !== 'number') this.end = Infinity;else if (Number.isNaN(this.end)) throw new TypeError('"end" option must be a Number'); if (!this.fd || !this.fd.type || this.fd.type !== 'static_fs_file_descriptor') this.open(); this.on('end', function () { if (this.autoClose) { this.destroy(); } }); } _util.default.inherits(ReadStream, _stream.Readable); ReadStream.prototype.open = function () { this._sfs.open(this.path, (er, fd) => { if (er) { if (this.autoClose) { this.destroy(); } this.emit('error', er); return; } this.fd = fd; this.emit('open', fd); this.emit('ready'); // start the flow of data. this.read(); }); }; ReadStream.prototype._read = function (n) { if (!this.fd || !this.fd.type || this.fd.type !== 'static_fs_file_descriptor') { return this.once('open', function () { this._read(n); }); } if (this.destroyed) return; if (!pool || pool.length - pool.used < kMinPoolSpace) { // discard the old pool. allocNewPool(this.readableHighWaterMark); } // Grab another reference to the pool in the case that while we're // in the thread pool another read() finishes up the pool, and // allocates a new one. const thisPool = pool; let toRead = Math.min(pool.length - pool.used, n); const start = pool.used; if (this.pos !== undefined) toRead = Math.min(this.end - this.pos + 1, toRead);else toRead = Math.min(this.end - this.bytesRead + 1, toRead); // already read everything we were supposed to read! // treat as EOF. if (toRead <= 0) return this.push(null); // the actual read. this._sfs.read(this.fd, pool, pool.used, toRead, this.pos, (er, bytesRead) => { if (er) { if (this.autoClose) { this.destroy(); } this.emit('error', er); } else { let b = null; // Now that we know how much data we have actually read, re-wind the // 'used' field if we can, and otherwise allow the remainder of our // reservation to be used as a new pool later. if (start + toRead === thisPool.used && thisPool === pool) thisPool.used += bytesRead - toRead;else if (toRead - bytesRead > kMinPoolSpace) poolFragments.push(thisPool.slice(start + bytesRead, start + toRead)); if (bytesRead > 0) { this.bytesRead += bytesRead; b = thisPool.slice(start, start + bytesRead); } this.push(b); } }); // move the pool positions, and internal position for reading. if (this.pos !== undefined) this.pos += toRead; pool.used += toRead; }; ReadStream.prototype._destroy = function (err, cb) { if (!this.fd || !this.fd.type || this.fd.type !== 'static_fs_file_descriptor') { this.once('open', closeFsStream.bind(null, this, cb, err)); return; } closeFsStream(this, cb, err); this.fd = null; }; function closeFsStream(stream, cb, err) { stream._sfs.close(stream.fd, er => { er = er || err; cb(er); stream.closed = true; if (!er) stream.emit('close'); }); } ReadStream.prototype.close = function (cb) { this.destroy(null, cb); }; Object.defineProperty(ReadStream.prototype, 'pending', { get() { return this.fd === null; }, configurable: true });