mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #12899 from overleaf/jpa-object-persistor-pipe
[object-persistor] move away from manual .pipe() GitOrigin-RevId: 5407d48fbbb026ba81f43c891499bd3a5ff59518
This commit is contained in:
parent
5638f06b3a
commit
2760ad35fc
10 changed files with 323 additions and 170 deletions
|
@ -19,7 +19,14 @@ module.exports = class AbstractPersistor {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// opts may be {start: Number, end: Number}
|
/**
|
||||||
|
* @param location
|
||||||
|
* @param name
|
||||||
|
* @param {Object} opts
|
||||||
|
* @param {Number} opts.start
|
||||||
|
* @param {Number} opts.end
|
||||||
|
* @return {Promise<Readable>}
|
||||||
|
*/
|
||||||
async getObjectStream(location, name, opts) {
|
async getObjectStream(location, name, opts) {
|
||||||
throw new NotImplementedError('method not implemented in persistor', {
|
throw new NotImplementedError('method not implemented in persistor', {
|
||||||
method: 'getObjectStream',
|
method: 'getObjectStream',
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const { pipeline } = require('stream/promises')
|
const { pipeline } = require('stream/promises')
|
||||||
|
const { PassThrough } = require('stream')
|
||||||
const { Storage } = require('@google-cloud/storage')
|
const { Storage } = require('@google-cloud/storage')
|
||||||
const { WriteError, ReadError, NotFoundError } = require('./Errors')
|
const { WriteError, ReadError, NotFoundError } = require('./Errors')
|
||||||
const asyncPool = require('tiny-async-pool')
|
const asyncPool = require('tiny-async-pool')
|
||||||
|
@ -101,16 +102,22 @@ module.exports = class GcsPersistor extends AbstractPersistor {
|
||||||
.file(key)
|
.file(key)
|
||||||
.createReadStream({ decompress: false, ...opts })
|
.createReadStream({ decompress: false, ...opts })
|
||||||
|
|
||||||
// ingress to us from gcs
|
|
||||||
const observer = new PersistorHelper.ObserverStream({
|
|
||||||
metric: 'gcs.ingress',
|
|
||||||
Metrics: this.settings.Metrics,
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// wait for the pipeline to be ready, to catch non-200s
|
await new Promise((resolve, reject) => {
|
||||||
await PersistorHelper.getReadyPipeline(stream, observer)
|
stream.on('response', res => {
|
||||||
return observer
|
switch (res.statusCode) {
|
||||||
|
case 200: // full response
|
||||||
|
case 206: // partial response
|
||||||
|
return resolve()
|
||||||
|
case 404:
|
||||||
|
return reject(new NotFoundError())
|
||||||
|
default:
|
||||||
|
return reject(new Error('non success status: ' + res.statusCode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
stream.on('error', reject)
|
||||||
|
stream.read(0) // kick off request
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw PersistorHelper.wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
|
@ -119,6 +126,16 @@ module.exports = class GcsPersistor extends AbstractPersistor {
|
||||||
ReadError
|
ReadError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ingress to us from gcs
|
||||||
|
const observer = new PersistorHelper.ObserverStream({
|
||||||
|
metric: 'gcs.ingress',
|
||||||
|
Metrics: this.settings.Metrics,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pass = new PassThrough()
|
||||||
|
pipeline(stream, observer, pass).catch(() => {})
|
||||||
|
return pass
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRedirectUrl(bucketName, key) {
|
async getRedirectUrl(bucketName, key) {
|
||||||
|
|
|
@ -2,7 +2,7 @@ const Crypto = require('crypto')
|
||||||
const Stream = require('stream')
|
const Stream = require('stream')
|
||||||
const { pipeline } = require('stream/promises')
|
const { pipeline } = require('stream/promises')
|
||||||
const Logger = require('@overleaf/logger')
|
const Logger = require('@overleaf/logger')
|
||||||
const { WriteError, ReadError, NotFoundError } = require('./Errors')
|
const { WriteError, NotFoundError } = require('./Errors')
|
||||||
|
|
||||||
// Observes data that passes through and computes some metadata for it
|
// Observes data that passes through and computes some metadata for it
|
||||||
// - specifically, it computes the number of bytes transferred, and optionally
|
// - specifically, it computes the number of bytes transferred, and optionally
|
||||||
|
@ -47,7 +47,6 @@ module.exports = {
|
||||||
ObserverStream,
|
ObserverStream,
|
||||||
calculateStreamMd5,
|
calculateStreamMd5,
|
||||||
verifyMd5,
|
verifyMd5,
|
||||||
getReadyPipeline,
|
|
||||||
wrapError,
|
wrapError,
|
||||||
hexToBase64,
|
hexToBase64,
|
||||||
base64ToHex,
|
base64ToHex,
|
||||||
|
@ -87,70 +86,6 @@ async function verifyMd5(persistor, bucket, key, sourceMd5, destMd5 = null) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolves when a stream is 'readable', or rejects if the stream throws an error
|
|
||||||
// before that happens - this lets us handle protocol-level errors before trying
|
|
||||||
// to read them
|
|
||||||
function getReadyPipeline(...streams) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const lastStream = streams.slice(-1)[0]
|
|
||||||
|
|
||||||
// in case of error or stream close, we must ensure that we drain the
|
|
||||||
// previous stream so that it can clean up its socket (if it has one)
|
|
||||||
const drainPreviousStream = function (previousStream) {
|
|
||||||
// this stream is no longer reliable, so don't pipe anything more into it
|
|
||||||
previousStream.unpipe(this)
|
|
||||||
previousStream.resume()
|
|
||||||
}
|
|
||||||
|
|
||||||
// handler to resolve when either:
|
|
||||||
// - an error happens, or
|
|
||||||
// - the last stream in the chain is readable
|
|
||||||
// for example, in the case of a 4xx error an error will occur and the
|
|
||||||
// streams will not become readable
|
|
||||||
const handler = function (err) {
|
|
||||||
// remove handler from all streams because we don't want to do this on
|
|
||||||
// later errors
|
|
||||||
lastStream.removeListener('readable', handler)
|
|
||||||
for (const stream of streams) {
|
|
||||||
stream.removeListener('error', handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// return control to the caller
|
|
||||||
if (err) {
|
|
||||||
reject(
|
|
||||||
wrapError(err, 'error before stream became ready', {}, ReadError)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
resolve(lastStream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure the handler fires when the last strem becomes readable
|
|
||||||
lastStream.on('readable', handler)
|
|
||||||
|
|
||||||
for (const stream of streams) {
|
|
||||||
// when a stream receives a pipe, set up the drain handler to drain the
|
|
||||||
// connection if an error occurs or the stream is closed
|
|
||||||
stream.on('pipe', previousStream => {
|
|
||||||
stream.on('error', x => {
|
|
||||||
drainPreviousStream(previousStream)
|
|
||||||
})
|
|
||||||
stream.on('close', () => {
|
|
||||||
drainPreviousStream(previousStream)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
// add the handler function to resolve this method on error if we can't
|
|
||||||
// set up the pipeline
|
|
||||||
stream.on('error', handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// begin the pipeline
|
|
||||||
for (let index = 0; index < streams.length - 1; index++) {
|
|
||||||
streams[index].pipe(streams[index + 1])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function wrapError(error, message, params, ErrorType) {
|
function wrapError(error, message, params, ErrorType) {
|
||||||
if (
|
if (
|
||||||
error instanceof NotFoundError ||
|
error instanceof NotFoundError ||
|
||||||
|
|
|
@ -10,6 +10,7 @@ if (https.globalAgent.maxSockets < 300) {
|
||||||
const AbstractPersistor = require('./AbstractPersistor')
|
const AbstractPersistor = require('./AbstractPersistor')
|
||||||
const PersistorHelper = require('./PersistorHelper')
|
const PersistorHelper = require('./PersistorHelper')
|
||||||
|
|
||||||
|
const { pipeline, PassThrough } = require('stream')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const S3 = require('aws-sdk/clients/s3')
|
const S3 = require('aws-sdk/clients/s3')
|
||||||
const { URL } = require('url')
|
const { URL } = require('url')
|
||||||
|
@ -41,7 +42,7 @@ module.exports = class S3Persistor extends AbstractPersistor {
|
||||||
|
|
||||||
const observer = new PersistorHelper.ObserverStream(observeOptions)
|
const observer = new PersistorHelper.ObserverStream(observeOptions)
|
||||||
// observer will catch errors, clean up and log a warning
|
// observer will catch errors, clean up and log a warning
|
||||||
readStream.pipe(observer)
|
pipeline(readStream, observer, () => {})
|
||||||
|
|
||||||
// if we have an md5 hash, pass this to S3 to verify the upload
|
// if we have an md5 hash, pass this to S3 to verify the upload
|
||||||
const uploadOptions = {
|
const uploadOptions = {
|
||||||
|
@ -90,20 +91,28 @@ module.exports = class S3Persistor extends AbstractPersistor {
|
||||||
params.Range = `bytes=${opts.start}-${opts.end}`
|
params.Range = `bytes=${opts.start}-${opts.end}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const stream = this._getClientForBucket(bucketName)
|
const req = this._getClientForBucket(bucketName).getObject(params)
|
||||||
.getObject(params)
|
const stream = req.createReadStream()
|
||||||
.createReadStream()
|
|
||||||
|
|
||||||
// ingress from S3 to us
|
|
||||||
const observer = new PersistorHelper.ObserverStream({
|
|
||||||
metric: 's3.ingress',
|
|
||||||
Metrics: this.settings.Metrics,
|
|
||||||
})
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// wait for the pipeline to be ready, to catch non-200s
|
await new Promise((resolve, reject) => {
|
||||||
await PersistorHelper.getReadyPipeline(stream, observer)
|
req.on('httpHeaders', statusCode => {
|
||||||
return observer
|
switch (statusCode) {
|
||||||
|
case 200: // full response
|
||||||
|
case 206: // partial response
|
||||||
|
return resolve()
|
||||||
|
case 403: // AccessDenied is handled the same as NoSuchKey
|
||||||
|
case 404: // NoSuchKey
|
||||||
|
return reject(new NotFoundError())
|
||||||
|
default:
|
||||||
|
return reject(new Error('non success status: ' + statusCode))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// The AWS SDK is forwarding any errors from the request to the stream.
|
||||||
|
// The AWS SDK is emitting additional errors on the stream ahead of starting to stream.
|
||||||
|
stream.on('error', reject)
|
||||||
|
// The AWS SDK is kicking off the request in the next event loop cycle.
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw PersistorHelper.wrapError(
|
throw PersistorHelper.wrapError(
|
||||||
err,
|
err,
|
||||||
|
@ -112,6 +121,18 @@ module.exports = class S3Persistor extends AbstractPersistor {
|
||||||
ReadError
|
ReadError
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ingress from S3 to us
|
||||||
|
const observer = new PersistorHelper.ObserverStream({
|
||||||
|
metric: 's3.ingress',
|
||||||
|
Metrics: this.settings.Metrics,
|
||||||
|
})
|
||||||
|
|
||||||
|
const pass = new PassThrough()
|
||||||
|
pipeline(stream, observer, pass, err => {
|
||||||
|
if (err) req.abort()
|
||||||
|
})
|
||||||
|
return pass
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRedirectUrl(bucketName, key) {
|
async getRedirectUrl(bucketName, key) {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const { EventEmitter } = require('events')
|
||||||
const sinon = require('sinon')
|
const sinon = require('sinon')
|
||||||
const chai = require('chai')
|
const chai = require('chai')
|
||||||
const { expect } = chai
|
const { expect } = chai
|
||||||
|
@ -21,6 +22,7 @@ describe('GcsPersistorTests', function () {
|
||||||
|
|
||||||
let Logger,
|
let Logger,
|
||||||
Transform,
|
Transform,
|
||||||
|
PassThrough,
|
||||||
Storage,
|
Storage,
|
||||||
Fs,
|
Fs,
|
||||||
GcsNotFoundError,
|
GcsNotFoundError,
|
||||||
|
@ -55,30 +57,28 @@ describe('GcsPersistorTests', function () {
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
ReadStream = {
|
class FakeGCSResponse extends EventEmitter {
|
||||||
pipe: sinon.stub().returns('readStream'),
|
constructor() {
|
||||||
on: sinon.stub(),
|
super()
|
||||||
removeListener: sinon.stub(),
|
this.statusCode = 200
|
||||||
|
this.err = null
|
||||||
}
|
}
|
||||||
ReadStream.on.withArgs('end').yields()
|
|
||||||
ReadStream.on.withArgs('pipe').yields({
|
read() {
|
||||||
unpipe: sinon.stub(),
|
if (this.err) return this.emit('error', this.err)
|
||||||
resume: sinon.stub(),
|
this.emit('response', { statusCode: this.statusCode })
|
||||||
on: sinon.stub(),
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
ReadStream = new FakeGCSResponse()
|
||||||
|
PassThrough = class {}
|
||||||
|
|
||||||
Transform = class {
|
Transform = class {
|
||||||
on(event, callback) {
|
|
||||||
if (event === 'readable') {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
once() {}
|
once() {}
|
||||||
removeListener() {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream = {
|
Stream = {
|
||||||
|
PassThrough,
|
||||||
Transform,
|
Transform,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,8 +155,8 @@ describe('GcsPersistorTests', function () {
|
||||||
stream = await GcsPersistor.getObjectStream(bucket, key)
|
stream = await GcsPersistor.getObjectStream(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a metered stream', function () {
|
it('returns a PassThrough stream', function () {
|
||||||
expect(stream).to.be.instanceOf(Transform)
|
expect(stream).to.be.instanceOf(PassThrough)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('fetches the right key from the right bucket', function () {
|
it('fetches the right key from the right bucket', function () {
|
||||||
|
@ -172,8 +172,10 @@ describe('GcsPersistorTests', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pipes the stream through the meter', function () {
|
it('pipes the stream through the meter', function () {
|
||||||
expect(ReadStream.pipe).to.have.been.calledWith(
|
expect(StreamPromises.pipeline).to.have.been.calledWith(
|
||||||
sinon.match.instanceOf(Transform)
|
ReadStream,
|
||||||
|
sinon.match.instanceOf(Transform),
|
||||||
|
sinon.match.instanceOf(PassThrough)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -188,8 +190,8 @@ describe('GcsPersistorTests', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a metered stream', function () {
|
it('returns a PassThrough stream', function () {
|
||||||
expect(stream).to.be.instanceOf(Transform)
|
expect(stream).to.be.instanceOf(PassThrough)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes the byte range on to GCS', function () {
|
it('passes the byte range on to GCS', function () {
|
||||||
|
@ -205,8 +207,7 @@ describe('GcsPersistorTests', function () {
|
||||||
let error, stream
|
let error, stream
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
Transform.prototype.on = sinon.stub()
|
ReadStream.statusCode = 404
|
||||||
ReadStream.on.withArgs('error').yields(GcsNotFoundError)
|
|
||||||
try {
|
try {
|
||||||
stream = await GcsPersistor.getObjectStream(bucket, key)
|
stream = await GcsPersistor.getObjectStream(bucket, key)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -231,12 +232,11 @@ describe('GcsPersistorTests', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when Gcs encounters an unkown error', function () {
|
describe('when Gcs encounters an unknown error', function () {
|
||||||
let error, stream
|
let error, stream
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
Transform.prototype.on = sinon.stub()
|
ReadStream.err = genericError
|
||||||
ReadStream.on.withArgs('error').yields(genericError)
|
|
||||||
try {
|
try {
|
||||||
stream = await GcsPersistor.getObjectStream(bucket, key)
|
stream = await GcsPersistor.getObjectStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -3,6 +3,7 @@ const chai = require('chai')
|
||||||
const { expect } = chai
|
const { expect } = chai
|
||||||
const SandboxedModule = require('sandboxed-module')
|
const SandboxedModule = require('sandboxed-module')
|
||||||
const Errors = require('../../src/Errors')
|
const Errors = require('../../src/Errors')
|
||||||
|
const { EventEmitter } = require('events')
|
||||||
|
|
||||||
const MODULE_PATH = '../../src/S3Persistor.js'
|
const MODULE_PATH = '../../src/S3Persistor.js'
|
||||||
|
|
||||||
|
@ -31,14 +32,15 @@ describe('S3PersistorTests', function () {
|
||||||
|
|
||||||
let Logger,
|
let Logger,
|
||||||
Transform,
|
Transform,
|
||||||
|
PassThrough,
|
||||||
S3,
|
S3,
|
||||||
Fs,
|
Fs,
|
||||||
ReadStream,
|
ReadStream,
|
||||||
Stream,
|
Stream,
|
||||||
StreamPromises,
|
StreamPromises,
|
||||||
|
S3GetObjectRequest,
|
||||||
S3Persistor,
|
S3Persistor,
|
||||||
S3Client,
|
S3Client,
|
||||||
S3ReadStream,
|
|
||||||
S3NotFoundError,
|
S3NotFoundError,
|
||||||
S3AccessDeniedError,
|
S3AccessDeniedError,
|
||||||
FileNotFoundError,
|
FileNotFoundError,
|
||||||
|
@ -55,18 +57,15 @@ describe('S3PersistorTests', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
Transform = class {
|
Transform = class {
|
||||||
on(event, callback) {
|
once() {}
|
||||||
if (event === 'readable') {
|
|
||||||
callback()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
once() {}
|
PassThrough = class {}
|
||||||
removeListener() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream = {
|
Stream = {
|
||||||
Transform,
|
Transform,
|
||||||
|
PassThrough,
|
||||||
|
pipeline: sinon.stub().yields(),
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamPromises = {
|
StreamPromises = {
|
||||||
|
@ -77,16 +76,28 @@ describe('S3PersistorTests', function () {
|
||||||
promise: sinon.stub().resolves(),
|
promise: sinon.stub().resolves(),
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadStream = {
|
ReadStream = new EventEmitter()
|
||||||
pipe: sinon.stub().returns('readStream'),
|
class FakeS3GetObjectRequest extends EventEmitter {
|
||||||
on: sinon.stub(),
|
constructor() {
|
||||||
removeListener: sinon.stub(),
|
super()
|
||||||
|
this.statusCode = 200
|
||||||
|
this.err = null
|
||||||
|
this.aborted = false
|
||||||
}
|
}
|
||||||
ReadStream.on.withArgs('end').yields()
|
|
||||||
ReadStream.on.withArgs('pipe').yields({
|
abort() {
|
||||||
unpipe: sinon.stub(),
|
this.aborted = true
|
||||||
resume: sinon.stub(),
|
}
|
||||||
|
|
||||||
|
createReadStream() {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.err) return ReadStream.emit('error', this.err)
|
||||||
|
this.emit('httpHeaders', this.statusCode)
|
||||||
})
|
})
|
||||||
|
return ReadStream
|
||||||
|
}
|
||||||
|
}
|
||||||
|
S3GetObjectRequest = new FakeS3GetObjectRequest()
|
||||||
|
|
||||||
FileNotFoundError = new Error('File not found')
|
FileNotFoundError = new Error('File not found')
|
||||||
FileNotFoundError.code = 'ENOENT'
|
FileNotFoundError.code = 'ENOENT'
|
||||||
|
@ -101,20 +112,8 @@ describe('S3PersistorTests', function () {
|
||||||
S3AccessDeniedError = new Error('access denied')
|
S3AccessDeniedError = new Error('access denied')
|
||||||
S3AccessDeniedError.code = 'AccessDenied'
|
S3AccessDeniedError.code = 'AccessDenied'
|
||||||
|
|
||||||
S3ReadStream = {
|
|
||||||
on: sinon.stub(),
|
|
||||||
pipe: sinon.stub(),
|
|
||||||
removeListener: sinon.stub(),
|
|
||||||
}
|
|
||||||
S3ReadStream.on.withArgs('end').yields()
|
|
||||||
S3ReadStream.on.withArgs('pipe').yields({
|
|
||||||
unpipe: sinon.stub(),
|
|
||||||
resume: sinon.stub(),
|
|
||||||
})
|
|
||||||
S3Client = {
|
S3Client = {
|
||||||
getObject: sinon.stub().returns({
|
getObject: sinon.stub().returns(S3GetObjectRequest),
|
||||||
createReadStream: sinon.stub().returns(S3ReadStream),
|
|
||||||
}),
|
|
||||||
headObject: sinon.stub().returns({
|
headObject: sinon.stub().returns({
|
||||||
promise: sinon.stub().resolves({
|
promise: sinon.stub().resolves({
|
||||||
ContentLength: objectSize,
|
ContentLength: objectSize,
|
||||||
|
@ -171,8 +170,8 @@ describe('S3PersistorTests', function () {
|
||||||
stream = await S3Persistor.getObjectStream(bucket, key)
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a metered stream', function () {
|
it('returns a PassThrough stream', function () {
|
||||||
expect(stream).to.be.instanceOf(Transform)
|
expect(stream).to.be.instanceOf(PassThrough)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the AWS client up with credentials from settings', function () {
|
it('sets the AWS client up with credentials from settings', function () {
|
||||||
|
@ -187,10 +186,16 @@ describe('S3PersistorTests', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('pipes the stream through the meter', async function () {
|
it('pipes the stream through the meter', async function () {
|
||||||
expect(S3ReadStream.pipe).to.have.been.calledWith(
|
expect(Stream.pipeline).to.have.been.calledWith(
|
||||||
sinon.match.instanceOf(Transform)
|
ReadStream,
|
||||||
|
sinon.match.instanceOf(Transform),
|
||||||
|
sinon.match.instanceOf(PassThrough)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('does not abort the request', function () {
|
||||||
|
expect(S3GetObjectRequest.aborted).to.equal(false)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when called with a byte range', function () {
|
describe('when called with a byte range', function () {
|
||||||
|
@ -203,8 +208,8 @@ describe('S3PersistorTests', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a metered stream', function () {
|
it('returns a PassThrough stream', function () {
|
||||||
expect(stream).to.be.instanceOf(Stream.Transform)
|
expect(stream).to.be.instanceOf(Stream.PassThrough)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('passes the byte range on to S3', function () {
|
it('passes the byte range on to S3', function () {
|
||||||
|
@ -216,6 +221,23 @@ describe('S3PersistorTests', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('when streaming fails', function () {
|
||||||
|
let stream
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
Stream.pipeline.yields(new Error())
|
||||||
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns a PassThrough stream', function () {
|
||||||
|
expect(stream).to.be.instanceOf(Stream.PassThrough)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('aborts the request', function () {
|
||||||
|
expect(S3GetObjectRequest.aborted).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('when there are alternative credentials', function () {
|
describe('when there are alternative credentials', function () {
|
||||||
let stream
|
let stream
|
||||||
const alternativeSecret = 'giraffe'
|
const alternativeSecret = 'giraffe'
|
||||||
|
@ -237,8 +259,8 @@ describe('S3PersistorTests', function () {
|
||||||
stream = await S3Persistor.getObjectStream(bucket, key)
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns a metered stream', function () {
|
it('returns a PassThrough stream', function () {
|
||||||
expect(stream).to.be.instanceOf(Stream.Transform)
|
expect(stream).to.be.instanceOf(Stream.PassThrough)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('sets the AWS client up with the alternative credentials', function () {
|
it('sets the AWS client up with the alternative credentials', function () {
|
||||||
|
@ -289,8 +311,7 @@ describe('S3PersistorTests', function () {
|
||||||
let error, stream
|
let error, stream
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
Transform.prototype.on = sinon.stub()
|
S3GetObjectRequest.statusCode = 404
|
||||||
S3ReadStream.on.withArgs('error').yields(S3NotFoundError)
|
|
||||||
try {
|
try {
|
||||||
stream = await S3Persistor.getObjectStream(bucket, key)
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -319,8 +340,7 @@ describe('S3PersistorTests', function () {
|
||||||
let error, stream
|
let error, stream
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
Transform.prototype.on = sinon.stub()
|
S3GetObjectRequest.statusCode = 403
|
||||||
S3ReadStream.on.withArgs('error').yields(S3AccessDeniedError)
|
|
||||||
try {
|
try {
|
||||||
stream = await S3Persistor.getObjectStream(bucket, key)
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -345,12 +365,11 @@ describe('S3PersistorTests', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when S3 encounters an unkown error', function () {
|
describe('when S3 encounters an unknown error', function () {
|
||||||
let error, stream
|
let error, stream
|
||||||
|
|
||||||
beforeEach(async function () {
|
beforeEach(async function () {
|
||||||
Transform.prototype.on = sinon.stub()
|
S3GetObjectRequest.err = genericError
|
||||||
S3ReadStream.on.withArgs('error').yields(genericError)
|
|
||||||
try {
|
try {
|
||||||
stream = await S3Persistor.getObjectStream(bucket, key)
|
stream = await S3Persistor.getObjectStream(bucket, key)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -480,7 +499,8 @@ describe('S3PersistorTests', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should meter the stream', function () {
|
it('should meter the stream', function () {
|
||||||
expect(ReadStream.pipe).to.have.been.calledWith(
|
expect(Stream.pipeline).to.have.been.calledWith(
|
||||||
|
ReadStream,
|
||||||
sinon.match.instanceOf(Stream.Transform)
|
sinon.match.instanceOf(Stream.Transform)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -608,12 +628,6 @@ describe('S3PersistorTests', function () {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should meter the download', function () {
|
|
||||||
expect(S3ReadStream.pipe).to.have.been.calledWith(
|
|
||||||
sinon.match.instanceOf(Stream.Transform)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should calculate the md5 hash from the file', function () {
|
it('should calculate the md5 hash from the file', function () {
|
||||||
expect(Hash.read).to.have.been.called
|
expect(Hash.read).to.have.been.called
|
||||||
})
|
})
|
||||||
|
|
30
patches/@google-cloud+storage++retry-request+5.0.2.patch
Normal file
30
patches/@google-cloud+storage++retry-request+5.0.2.patch
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
diff --git a/node_modules/@google-cloud/storage/node_modules/retry-request/index.js b/node_modules/@google-cloud/storage/node_modules/retry-request/index.js
|
||||||
|
index a293298..df21af6 100644
|
||||||
|
--- a/node_modules/@google-cloud/storage/node_modules/retry-request/index.js
|
||||||
|
+++ b/node_modules/@google-cloud/storage/node_modules/retry-request/index.js
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
-const {PassThrough} = require('stream');
|
||||||
|
+const { PassThrough, pipeline } = require('stream');
|
||||||
|
const debug = require('debug')('retry-request');
|
||||||
|
const extend = require('extend');
|
||||||
|
|
||||||
|
@@ -166,7 +166,7 @@ function retryRequest(requestOpts, opts, callback) {
|
||||||
|
})
|
||||||
|
.on('complete', retryStream.emit.bind(retryStream, 'complete'));
|
||||||
|
|
||||||
|
- requestStream.pipe(delayStream);
|
||||||
|
+ pipeline(requestStream, delayStream, () => {});
|
||||||
|
} else {
|
||||||
|
activeRequest = opts.request(requestOpts, onResponse);
|
||||||
|
}
|
||||||
|
@@ -232,7 +232,7 @@ function retryRequest(requestOpts, opts, callback) {
|
||||||
|
// No more attempts need to be made, just continue on.
|
||||||
|
if (streamMode) {
|
||||||
|
retryStream.emit('response', response);
|
||||||
|
- delayStream.pipe(retryStream);
|
||||||
|
+ pipeline(delayStream, retryStream, () => {});
|
||||||
|
requestStream.on('error', err => {
|
||||||
|
retryStream.destroy(err);
|
||||||
|
});
|
50
patches/@google-cloud+storage++teeny-request+8.0.2.patch
Normal file
50
patches/@google-cloud+storage++teeny-request+8.0.2.patch
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
diff --git a/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js b/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
|
||||||
|
index a2251ca..e29e796 100644
|
||||||
|
--- a/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
|
||||||
|
+++ b/node_modules/@google-cloud/storage/node_modules/teeny-request/build/src/index.js
|
||||||
|
@@ -166,27 +166,27 @@ function teenyRequest(reqOpts, callback) {
|
||||||
|
}
|
||||||
|
if (callback === undefined) {
|
||||||
|
// Stream mode
|
||||||
|
- const requestStream = streamEvents(new stream_1.PassThrough());
|
||||||
|
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
- let responseStream;
|
||||||
|
- requestStream.once('reading', () => {
|
||||||
|
- if (responseStream) {
|
||||||
|
- responseStream.pipe(requestStream);
|
||||||
|
- }
|
||||||
|
- else {
|
||||||
|
- requestStream.once('response', () => {
|
||||||
|
- responseStream.pipe(requestStream);
|
||||||
|
- });
|
||||||
|
- }
|
||||||
|
- });
|
||||||
|
+ const requestStream = new stream_1.PassThrough();
|
||||||
|
+ // // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
+ // let responseStream;
|
||||||
|
+ // requestStream.once('reading', () => {
|
||||||
|
+ // if (responseStream) {
|
||||||
|
+ // responseStream.pipe(requestStream);
|
||||||
|
+ // }
|
||||||
|
+ // else {
|
||||||
|
+ // requestStream.once('response', () => {
|
||||||
|
+ // responseStream.pipe(requestStream);
|
||||||
|
+ // });
|
||||||
|
+ // }
|
||||||
|
+ // });
|
||||||
|
options.compress = false;
|
||||||
|
teenyRequest.stats.requestStarting();
|
||||||
|
(0, node_fetch_1.default)(uri, options).then(res => {
|
||||||
|
- teenyRequest.stats.requestFinished();
|
||||||
|
- responseStream = res.body;
|
||||||
|
- responseStream.on('error', (err) => {
|
||||||
|
- requestStream.emit('error', err);
|
||||||
|
- });
|
||||||
|
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
|
||||||
|
+ // responseStream = res.body;
|
||||||
|
+ // responseStream.on('error', (err) => {
|
||||||
|
+ // requestStream.emit('error', err);
|
||||||
|
+ // });
|
||||||
|
const response = fetchToRequestResponse(options, res);
|
||||||
|
requestStream.emit('response', response);
|
||||||
|
}, err => {
|
30
patches/retry-request+4.2.2.patch
Normal file
30
patches/retry-request+4.2.2.patch
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
diff --git a/node_modules/retry-request/index.js b/node_modules/retry-request/index.js
|
||||||
|
index 6cd6f65..39efb89 100644
|
||||||
|
--- a/node_modules/retry-request/index.js
|
||||||
|
+++ b/node_modules/retry-request/index.js
|
||||||
|
@@ -1,6 +1,6 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
-var { PassThrough } = require('stream');
|
||||||
|
+var { PassThrough, pipeline } = require('stream');
|
||||||
|
var debug = require('debug')('retry-request');
|
||||||
|
var extend = require('extend');
|
||||||
|
|
||||||
|
@@ -164,7 +164,7 @@ function retryRequest(requestOpts, opts, callback) {
|
||||||
|
})
|
||||||
|
.on('complete', retryStream.emit.bind(retryStream, 'complete'));
|
||||||
|
|
||||||
|
- requestStream.pipe(delayStream);
|
||||||
|
+ pipeline(requestStream, delayStream, () => {});
|
||||||
|
} else {
|
||||||
|
activeRequest = opts.request(requestOpts, onResponse);
|
||||||
|
}
|
||||||
|
@@ -220,7 +220,7 @@ function retryRequest(requestOpts, opts, callback) {
|
||||||
|
// No more attempts need to be made, just continue on.
|
||||||
|
if (streamMode) {
|
||||||
|
retryStream.emit('response', response);
|
||||||
|
- delayStream.pipe(retryStream);
|
||||||
|
+ pipeline(delayStream, retryStream, () => {});
|
||||||
|
requestStream.on('error', function (err) {
|
||||||
|
retryStream.destroy(err);
|
||||||
|
});
|
49
patches/teeny-request+7.1.3.patch
Normal file
49
patches/teeny-request+7.1.3.patch
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
diff --git a/node_modules/teeny-request/build/src/index.js b/node_modules/teeny-request/build/src/index.js
|
||||||
|
index f209888..e9fe982 100644
|
||||||
|
--- a/node_modules/teeny-request/build/src/index.js
|
||||||
|
+++ b/node_modules/teeny-request/build/src/index.js
|
||||||
|
@@ -166,27 +166,27 @@ function teenyRequest(reqOpts, callback) {
|
||||||
|
}
|
||||||
|
if (callback === undefined) {
|
||||||
|
// Stream mode
|
||||||
|
- const requestStream = streamEvents(new stream_1.PassThrough());
|
||||||
|
+ const requestStream = new stream_1.PassThrough();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
- let responseStream;
|
||||||
|
- requestStream.once('reading', () => {
|
||||||
|
- if (responseStream) {
|
||||||
|
- responseStream.pipe(requestStream);
|
||||||
|
- }
|
||||||
|
- else {
|
||||||
|
- requestStream.once('response', () => {
|
||||||
|
- responseStream.pipe(requestStream);
|
||||||
|
- });
|
||||||
|
- }
|
||||||
|
- });
|
||||||
|
+ // let responseStream;
|
||||||
|
+ // requestStream.once('reading', () => {
|
||||||
|
+ // if (responseStream) {
|
||||||
|
+ // responseStream.pipe(requestStream);
|
||||||
|
+ // }
|
||||||
|
+ // else {
|
||||||
|
+ // requestStream.once('response', () => {
|
||||||
|
+ // responseStream.pipe(requestStream);
|
||||||
|
+ // });
|
||||||
|
+ // }
|
||||||
|
+ // });
|
||||||
|
options.compress = false;
|
||||||
|
teenyRequest.stats.requestStarting();
|
||||||
|
node_fetch_1.default(uri, options).then(res => {
|
||||||
|
- teenyRequest.stats.requestFinished();
|
||||||
|
- responseStream = res.body;
|
||||||
|
- responseStream.on('error', (err) => {
|
||||||
|
- requestStream.emit('error', err);
|
||||||
|
- });
|
||||||
|
+ teenyRequest.stats.requestFinished(); stream_1.pipeline(res.body, requestStream, () => {});
|
||||||
|
+ // responseStream = res.body;
|
||||||
|
+ // responseStream.on('error', (err) => {
|
||||||
|
+ // requestStream.emit('error', err);
|
||||||
|
+ // });
|
||||||
|
const response = fetchToRequestResponse(options, res);
|
||||||
|
requestStream.emit('response', response);
|
||||||
|
}, err => {
|
Loading…
Reference in a new issue