UNPKG

tus-js-client

Version:

A pure JavaScript client for the tus resumable upload protocol

213 lines (179 loc) 6.26 kB
// The url.parse method is superseeded by the url.URL constructor, // but it is still included in Node.js import * as http from 'http' import * as https from 'https' import { Readable, Transform } from 'stream' import { parse } from 'url' import throttle from 'lodash.throttle' export default class NodeHttpStack { constructor(requestOptions = {}) { this._requestOptions = requestOptions } createRequest(method, url) { return new Request(method, url, this._requestOptions) } getName() { return 'NodeHttpStack' } } class Request { constructor(method, url, options) { this._method = method this._url = url this._headers = {} this._request = null this._progressHandler = () => {} this._requestOptions = options || {} } getMethod() { return this._method } getURL() { return this._url } setHeader(header, value) { this._headers[header] = value } getHeader(header) { return this._headers[header] } setProgressHandler(progressHandler) { this._progressHandler = progressHandler } send(body = null) { return new Promise((resolve, reject) => { const options = { ...parse(this._url), ...this._requestOptions, method: this._method, headers: { ...(this._requestOptions.headers || {}), ...this._headers, }, } if (body?.size) { options.headers['Content-Length'] = body.size } const httpModule = options.protocol === 'https:' ? https : http this._request = httpModule.request(options) const req = this._request req.on('response', (res) => { const resChunks = [] res.on('data', (data) => { resChunks.push(data) }) res.on('end', () => { const responseText = Buffer.concat(resChunks).toString('utf8') resolve(new Response(res, responseText)) }) }) req.on('error', (err) => { reject(err) }) if (body instanceof Readable) { // Readable stream are piped through a PassThrough instance, which // counts the number of bytes passed through. This is used, for example, // when an fs.ReadStream is provided to tus-js-client. body.pipe(new ProgressEmitter(this._progressHandler)).pipe(req) } else if (body instanceof Uint8Array) { // For Buffers and Uint8Arrays (in Node.js all buffers are instances of Uint8Array), // we write chunks of the buffer to the stream and use that to track the progress. // This is used when either a Buffer or a normal readable stream is provided // to tus-js-client. writeBufferToStreamWithProgress(req, body, this._progressHandler) } else { req.end(body) } }) } abort() { if (this._request !== null) this._request.abort() return Promise.resolve() } getUnderlyingObject() { return this._request } } class Response { constructor(res, body) { this._response = res this._body = body } getStatus() { return this._response.statusCode } getHeader(header) { return this._response.headers[header.toLowerCase()] } getBody() { return this._body } getUnderlyingObject() { return this._response } } // ProgressEmitter is a simple PassThrough-style transform stream which keeps // track of the number of bytes which have been piped through it and will // invoke the `onprogress` function whenever new number are available. class ProgressEmitter extends Transform { constructor(onprogress) { super() // The _onprogress property will be invoked, whenever a chunk is piped // through this transformer. Since chunks are usually quite small (64kb), // these calls can occur frequently, especially when you have a good // connection to the remote server. Therefore, we are throtteling them to // prevent accessive function calls. this._onprogress = throttle(onprogress, 100, { leading: true, trailing: false, }) this._position = 0 } _transform(chunk, _encoding, callback) { this._position += chunk.length this._onprogress(this._position) callback(null, chunk) } } // writeBufferToStreamWithProgress writes chunks from `source` (either a // Buffer or Uint8Array) to the readable stream `stream`. // The size of the chunk depends on the stream's highWaterMark to fill the // stream's internal buffer as best as possible. // If the internal buffer is full, the callback `onprogress` will be invoked // to notify about the write progress. Writing will be resumed once the internal // buffer is empty, as indicated by the emitted `drain` event. // See https://nodejs.org/docs/latest/api/stream.html#buffering for more details // on the buffering behavior of streams. const writeBufferToStreamWithProgress = (stream, source, onprogress) => { onprogress = throttle(onprogress, 100, { leading: true, trailing: false, }) let offset = 0 function writeNextChunk() { // Take at most the amount of bytes from highWaterMark. This should fill the streams // internal buffer already. const chunkSize = Math.min(stream.writableHighWaterMark, source.length - offset) // Note: We use subarray instead of slice because it works without copying data for // Buffers and Uint8Arrays. const chunk = source.subarray(offset, offset + chunkSize) offset += chunk.length // `write` returns true if the internal buffer is not full and we should write more. // If the stream is destroyed because the request is aborted, it will return false // and no 'drain' event is emitted, so won't continue writing data. const canContinue = stream.write(chunk) if (!canContinue) { // If the buffer is full, wait for the 'drain' event to write more data. stream.once('drain', writeNextChunk) onprogress(offset) } else if (offset < source.length) { // If there's still data to write and the buffer is not full, write next chunk. writeNextChunk() } else { // If all data has been written, close the stream if needed, and emit a 'finish' event. stream.end() } } // Start writing the first chunk. writeNextChunk() }