From 18ede8c65dec28346980ec55eaf23fdc36211d1e Mon Sep 17 00:00:00 2001 From: Mukul Mishra Date: Sun, 30 Jul 2017 20:28:32 +0530 Subject: [PATCH] Adds http support to node_stream logic --- src/display/network.js | 55 ++---- src/display/network_utils.js | 56 +++++++ src/display/node_stream.js | 313 ++++++++++++++++++++++++++--------- 3 files changed, 309 insertions(+), 115 deletions(-) create mode 100644 src/display/network_utils.js diff --git a/src/display/network.js b/src/display/network.js index 5397d161a..cdb375398 100644 --- a/src/display/network.js +++ b/src/display/network.js @@ -18,6 +18,7 @@ import { UnexpectedResponseException } from '../shared/util'; import globalScope from '../shared/global_scope'; +import { validateRangeRequestCapabilities } from './network_utils'; if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('FIREFOX || MOZCENTRAL')) { throw new Error('Module "./network" shall not ' + @@ -351,51 +352,27 @@ function PDFNetworkStreamFullRequestReader(manager, options) { } PDFNetworkStreamFullRequestReader.prototype = { - _validateRangeRequestCapabilities: function - PDFNetworkStreamFullRequestReader_validateRangeRequestCapabilities() { + getResponseHeader(name) { + let fullRequestXhrId = this._fullRequestId; + let fullRequestXhr = this._manager.getRequestXhr(fullRequestXhrId); - if (this._disableRange) { - return false; - } - - var networkManager = this._manager; - if (!networkManager.isHttp) { - return false; - } - var fullRequestXhrId = this._fullRequestId; - var fullRequestXhr = networkManager.getRequestXhr(fullRequestXhrId); - if (fullRequestXhr.getResponseHeader('Accept-Ranges') !== 'bytes') { - return false; - } - - var contentEncoding = - fullRequestXhr.getResponseHeader('Content-Encoding') || 'identity'; - if (contentEncoding !== 'identity') { - return false; - } - - var length = fullRequestXhr.getResponseHeader('Content-Length'); - length = parseInt(length, 10); - if (!isInt(length)) { - return false; - } - - this._contentLength = length; // setting right content length - - if (length <= 2 * this._rangeChunkSize) { - // The file size is smaller than the size of two chunks, so it does - // not make any sense to abort the request and retry with a range - // request. - return false; - } - - return true; + return fullRequestXhr.getResponseHeader(name); }, _onHeadersReceived: function PDFNetworkStreamFullRequestReader_onHeadersReceived() { + let { allowRangeRequests, suggestedLength, } = + validateRangeRequestCapabilities({ + getResponseHeader: this.getResponseHeader.bind(this), + isHttp: this._manager.isHttp, + rangeChunkSize: this._rangeChunkSize, + disableRange: this._disableRange, + }); - if (this._validateRangeRequestCapabilities()) { + // Setting right content length. + this._contentLength = suggestedLength || this._contentLength; + + if (allowRangeRequests) { this._isRangeSupported = true; } diff --git a/src/display/network_utils.js b/src/display/network_utils.js new file mode 100644 index 000000000..9d8ebe1c0 --- /dev/null +++ b/src/display/network_utils.js @@ -0,0 +1,56 @@ +/* Copyright 2012 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { isInt } from '../shared/util'; + +function validateRangeRequestCapabilities({ getResponseHeader, isHttp, + rangeChunkSize, disableRange, }) { + let returnValues = { + allowRangeRequests: false, + suggestedLength: undefined, + }; + if (disableRange || !isHttp) { + return returnValues; + } + if (getResponseHeader('Accept-Ranges') !== 'bytes') { + return returnValues; + } + + let contentEncoding = getResponseHeader('Content-Encoding') || 'identity'; + if (contentEncoding !== 'identity') { + return returnValues; + } + + let length = getResponseHeader('Content-Length'); + length = parseInt(length, 10); + if (!isInt(length)) { + return returnValues; + } + + returnValues.suggestedLength = length; + if (length <= 2 * rangeChunkSize) { + // The file size is smaller than the size of two chunks, so it does + // not make any sense to abort the request and retry with a range + // request. + return returnValues; + } + + returnValues.allowRangeRequests = true; + return returnValues; +} + +export { + validateRangeRequestCapabilities, +}; diff --git a/src/display/node_stream.js b/src/display/node_stream.js index 09846ac23..009157338 100644 --- a/src/display/node_stream.js +++ b/src/display/node_stream.js @@ -15,26 +15,39 @@ /* globals __non_webpack_require__ */ let fs = __non_webpack_require__('fs'); -let utils = require('../shared/util'); +let http = __non_webpack_require__('http'); +let https = __non_webpack_require__('https'); +let url = __non_webpack_require__('url'); -let assert = utils.assert; -let createPromiseCapability = utils.createPromiseCapability; +import { assert, createPromiseCapability } from '../shared/util'; +import { validateRangeRequestCapabilities } from './network_utils'; class PDFNodeStream { - constructor(args) { - this._path = args.path; + constructor(options) { + this.options = options; + this.source = options.source; + this.url = url.parse(this.source.url); + this.isHttp = this.url.protocol === 'http:' || + this.url.protocol === 'https:'; + this.isFsUrl = !this.url.host; + this.httpHeaders = (this.isHttp && this.source.httpHeaders) || {}; + this._fullRequest = null; this._rangeRequestReaders = []; } getFullReader() { assert(!this._fullRequest); - this._fullRequest = new PDFNodeStreamFullReader(this._path); + this._fullRequest = this.isFsUrl ? + new PDFNodeStreamFsFullReader(this) : + new PDFNodeStreamFullReader(this); return this._fullRequest; } - getRangeReader(begin, end) { - let rangeReader = new PDFNodeStreamRangeReader(this._path, begin, end); + getRangeReader(start, end) { + let rangeReader = this.isFsUrl ? + new PDFNodeStreamFsRangeReader(this, start, end) : + new PDFNodeStreamRangeReader(this, start, end); this._rangeRequestReaders.push(rangeReader); return rangeReader; } @@ -51,44 +64,19 @@ class PDFNodeStream { } } -class PDFNodeStreamFullReader { - constructor(path) { - this._path = path; +class BaseFullReader { + constructor(stream) { + this._url = stream.url; this._done = false; this._errored = false; this._reason = null; this.onProgress = null; - this._length = null; + this._length = stream.source.length; this._loaded = 0; + + this._fullRequest = null; this._readCapability = createPromiseCapability(); this._headersCapability = createPromiseCapability(); - this._fullRequest = fs.createReadStream(path); - - fs.lstat(this._path, (error, stat) => { - if (error) { - this._errored = true; - this._reason = error; - this._headersCapability.reject(error); - return; - } - this._length = stat.size; - this._headersCapability.resolve(); - }); - - this._fullRequest.on('readable', () => { - this._readCapability.resolve(); - }); - - this._fullRequest.on('end', () => { - this._done = true; - this._readCapability.resolve(); - }); - - this._fullRequest.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._readCapability.resolve(); - }); } get headersReady() { @@ -100,11 +88,11 @@ class PDFNodeStreamFullReader { } get isRangeSupported() { - return true; + return this._isRangeSupported; } get isStreamingSupported() { - return true; + return this._isStreamingSupported; } read() { @@ -138,45 +126,20 @@ class PDFNodeStreamFullReader { } } -class PDFNodeStreamRangeReader { - constructor(path, start, end) { - this._path = path; +class BaseRangeReader { + constructor(stream) { + this._url = stream.url; this._done = false; this._errored = false; this._reason = null; this.onProgress = null; - this._length = null; + this._length = stream.source.length; this._loaded = 0; this._readCapability = createPromiseCapability(); - this._rangeRequest = fs.createReadStream(path, { start, end, }); - - fs.lstat(this._path, (error, stat) => { - if (error) { - this._errored = true; - this._reason = error; - return; - } - this._length = stat.size; - }); - - this._rangeRequest.on('readable', () => { - this._readCapability.resolve(); - }); - - this._rangeRequest.on('end', () => { - this._done = true; - this._readCapability.resolve(); - }); - - this._rangeRequest.on('error', (reason) => { - this._errored = true; - this._reason = reason; - this._readCapability.resolve(); - }); } get isStreamingSupported() { - return true; + return false; } read() { @@ -188,7 +151,7 @@ class PDFNodeStreamRangeReader { return Promise.reject(this._reason); } - let chunk = this._rangeRequest.read(); + let chunk = this._read(); if (chunk === null) { this._readCapability = createPromiseCapability(); return this.read(); @@ -203,11 +166,209 @@ class PDFNodeStreamRangeReader { return Promise.resolve({ value: chunk, done: false, }); }); } +} - cancel(reason) { - this._fullRequest.cancel(reason); - this._fullRequest.destroy(reason); +class PDFNodeStreamFullReader extends BaseFullReader { + constructor(stream) { + super(stream); + + this._disableRange = stream.options.disableRange || false; + this._rangeChunkSize = stream.source.rangeChunkSize; + if (!this._rangeChunkSize && !this._disableRange) { + this._disableRange = true; + } + + this._isStreamingSupported = !stream.source.disableStream; + this._isRangeSupported = false; + + let options = { + host: this._url.host, + path: this._url.path, + method: 'GET', + headers: stream.httpHeaders, + }; + + let handleResponse = (response) => { + this._headersCapability.resolve(); + this._fullRequest = response; + + response.on('readable', () => { + this._readCapability.resolve(); + }); + + response.on('end', () => { + // Destroy response to minimize resource usage. + response.destroy(); + this._done = true; + this._readCapability.resolve(); + }); + + response.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._readCapability.resolve(); + }); + }; + + this._request = this._url.protocol === 'http:' ? + http.request(options, handleResponse) : + https.request(options, handleResponse); + + this._request.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._headersCapability.reject(reason); + }); + this._request.end(); + + this._headersCapability.promise.then(() => { + let { allowRangeRequests, suggestedLength, } = + validateRangeRequestCapabilities({ + getResponseHeader: this.getResponseHeader.bind(this), + isHttp: stream.isHttp, + rangeChunkSize: this._rangeChunkSize, + disableRange: this._disableRange, + }); + + if (allowRangeRequests) { + this._isRangeSupported = true; + } + this._length = suggestedLength; + }); + } + + getReasponseHeader(name) { + return this._fullRequest.headers[name]; } } -exports.PDFNodeStream = PDFNodeStream; +class PDFNodeStreamRangeReader extends BaseRangeReader { + constructor(stream, start, end) { + super(stream); + + this._rangeRequest = null; + this._read = null; + let rangeStr = start + '-' + (end - 1); + stream.httpHeaders['Range'] = 'bytes=' + rangeStr; + + let options = { + host: this._url.host, + path: this._url.path, + method: 'GET', + headers: stream.httpHeaders, + }; + let handleResponse = (response) => { + this._rangeRequest = response; + this._read = this._rangeRequest.read; + + response.on('readable', () => { + this._readCapability.resolve(); + }); + + response.on('end', () => { + response.destroy(); + this._done = true; + this._readCapability.resolve(); + }); + + response.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._readCapability.resolve(); + }); + }; + this._request = this._url.protocol === 'http:' ? + http.request(options, handleResponse) : + https.request(options, handleResponse); + + this._request.on('error', (reason) => { + this._errored = true; + this._reason = reason; + }); + this._request.end(); + } + + cancel(reason) { + this._rangeRequest.close(reason); + this._rangeRequest.destroy(reason); + } +} + +class PDFNodeStreamFsFullReader extends BaseFullReader { + constructor(stream) { + super(stream); + + this._isRangeSupported = true; + this._isStreamingSupported = true; + this._fullRequest = fs.createReadStream(this._url.path); + + fs.lstat(this._url.path, (error, stat) => { + if (error) { + this._errored = true; + this._reason = error; + this._headersCapability.reject(error); + return; + } + this._length = stat.size; + this._headersCapability.resolve(); + }); + + this._fullRequest.on('readable', () => { + this._readCapability.resolve(); + }); + + this._fullRequest.on('end', () => { + this._fullRequest.destroy(); + this._done = true; + this._readCapability.resolve(); + }); + + this._fullRequest.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._readCapability.resolve(); + }); + } +} + +class PDFNodeStreamFsRangeReader extends BaseRangeReader { + constructor(stream, start, end) { + super(stream); + + this._rangeRequest = fs.createReadStream(this._url.path, { start, end, }); + fs.lstat(this._url.path, (error, stat) => { + if (error) { + this._errored = true; + this._reason = error; + return; + } + this._length = stat.size; + }); + this._read = this._rangeRequest.read; + + this._rangeRequest.on('readable', () => { + this._readCapability.resolve(); + }); + + this._rangeRequest.on('end', () => { + this._rangeRequest.destroy(); + this._done = true; + this._readCapability.resolve(); + }); + + this._rangeRequest.on('error', (reason) => { + this._errored = true; + this._reason = reason; + this._readCapability.resolve(); + }); + } + + cancel(reason) { + this._rangeRequest.close(reason); + this._rangeRequest.destroy(reason); + } +} + +export { + PDFNodeStream, +};