diff --git a/packages/@uppy/aws-s3-multipart/src/index.js b/packages/@uppy/aws-s3-multipart/src/index.js index 779cba4b7f..894e260a73 100644 --- a/packages/@uppy/aws-s3-multipart/src/index.js +++ b/packages/@uppy/aws-s3-multipart/src/index.js @@ -803,6 +803,16 @@ export default class AwsS3Multipart extends BasePlugin { }) } + // eslint-disable-next-line class-methods-use-this + #getCompanionClientArgs (file) { + return { + ...file.remote.body, + protocol: 's3-multipart', + size: file.data.size, + metadata: file.meta, + } + } + #upload = async (fileIDs) => { if (fileIDs.length === 0) return undefined @@ -816,7 +826,8 @@ export default class AwsS3Multipart extends BasePlugin { if (file.isRemote) { // TODO: why do we need to do this? why not always one or the other? const Client = file.remote.providerOptions.provider ? Provider : RequestClient - const client = new Client(this.uppy, file.remote.providerOptions) + const getQueue = () => this.requests + const client = new Client(this.uppy, file.remote.providerOptions, getQueue) this.#setResumableUploadsCapability(false) const controller = new AbortController() @@ -825,7 +836,11 @@ export default class AwsS3Multipart extends BasePlugin { } this.uppy.on('file-removed', removedHandler) - const uploadPromise = client.uploadRemoteFile(file, { signal: controller.signal }, this.requests) + const uploadPromise = client.uploadRemoteFile( + file, + this.#getCompanionClientArgs(file), + { signal: controller.signal }, + ) this.requests.wrapSyncFunction(() => { this.uppy.off('file-removed', removedHandler) diff --git a/packages/@uppy/aws-s3/src/index.js b/packages/@uppy/aws-s3/src/index.js index fa66676c72..65727b117e 100644 --- a/packages/@uppy/aws-s3/src/index.js +++ b/packages/@uppy/aws-s3/src/index.js @@ -247,6 +247,30 @@ export default class AwsS3 extends BasePlugin { return Promise.resolve() } + #getCompanionClientArgs = (file) => { + const opts = this.#uploader.getOptions(file) + const allowedMetaFields = Array.isArray(opts.allowedMetaFields) + ? opts.allowedMetaFields + // Send along all fields by default. + : Object.keys(file.meta) + // TODO: do we need tus in aws-s3? + if (file.tus) { + // Install file-specific upload overrides. + Object.assign(opts, file.tus) + } + return { + ...file.remote.body, + protocol: 'multipart', + endpoint: opts.endpoint, + size: file.data.size, + fieldname: opts.fieldName, + metadata: Object.fromEntries(allowedMetaFields.map(name => [name, file.meta[name]])), + httpMethod: opts.method, + useFormData: opts.formData, + headers: opts.headers, + } + } + uploadFile (id, current, total) { const file = this.uppy.getFile(id) this.uppy.log(`uploading ${current} of ${total}`) @@ -256,7 +280,8 @@ export default class AwsS3 extends BasePlugin { if (file.isRemote) { // TODO: why do we need to do this? why not always one or the other? const Client = file.remote.providerOptions.provider ? Provider : RequestClient - const client = new Client(this.uppy, file.remote.providerOptions) + const getQueue = () => this.#requests + const client = new Client(this.uppy, file.remote.providerOptions, getQueue) const controller = new AbortController() const removedHandler = (removedFile) => { @@ -264,7 +289,11 @@ export default class AwsS3 extends BasePlugin { } this.uppy.on('file-removed', removedHandler) - const uploadPromise = client.uploadRemoteFile(file, { signal: controller.signal }, this.#requests) + const uploadPromise = client.uploadRemoteFile( + file, + this.#getCompanionClientArgs(file), + { signal: controller.signal }, + ) this.#requests.wrapSyncFunction(() => { this.uppy.off('file-removed', removedHandler) diff --git a/packages/@uppy/companion-client/src/Provider.js b/packages/@uppy/companion-client/src/Provider.js index 081f3cdc65..3a4dda9d69 100644 --- a/packages/@uppy/companion-client/src/Provider.js +++ b/packages/@uppy/companion-client/src/Provider.js @@ -10,8 +10,8 @@ const getName = (id) => { export default class Provider extends RequestClient { #refreshingTokenPromise - constructor (uppy, opts) { - super(uppy, opts) + constructor (uppy, opts, getQueue) { + super(uppy, opts, getQueue) this.provider = opts.provider this.id = this.provider this.name = this.opts.name || getName(this.id) diff --git a/packages/@uppy/companion-client/src/RequestClient.js b/packages/@uppy/companion-client/src/RequestClient.js index f812d92f64..8f96224d8d 100644 --- a/packages/@uppy/companion-client/src/RequestClient.js +++ b/packages/@uppy/companion-client/src/RequestClient.js @@ -30,8 +30,12 @@ async function handleJSONResponse (res) { try { const errData = await jsonPromise errMsg = errData.message ? `${errMsg} message: ${errData.message}` : errMsg - errMsg = errData.requestId ? `${errMsg} request-Id: ${errData.requestId}` : errMsg - } catch { /* if the response contains invalid JSON, let's ignore the error */ } + errMsg = errData.requestId + ? `${errMsg} request-Id: ${errData.requestId}` + : errMsg + } catch { + /* if the response contains invalid JSON, let's ignore the error */ + } throw new Error(errMsg) } @@ -43,9 +47,10 @@ export default class RequestClient { #companionHeaders - constructor (uppy, opts) { + constructor (uppy, opts, getQueue) { this.uppy = uppy this.opts = opts + this.getQueue = getQueue this.onReceiveResponse = this.onReceiveResponse.bind(this) this.#companionHeaders = opts?.companionHeaders } @@ -54,7 +59,9 @@ export default class RequestClient { this.#companionHeaders = headers } - [Symbol.for('uppy test: getCompanionHeaders')] () { return this.#companionHeaders } + [Symbol.for('uppy test: getCompanionHeaders')] () { + return this.#companionHeaders + } get hostname () { const { companion } = this.uppy.getState() @@ -113,7 +120,11 @@ export default class RequestClient { const allowedHeadersCached = allowedHeadersCache.get(this.hostname) if (allowedHeadersCached != null) return allowedHeadersCached - const fallbackAllowedHeaders = ['accept', 'content-type', 'uppy-auth-token'] + const fallbackAllowedHeaders = [ + 'accept', + 'content-type', + 'uppy-auth-token', + ] const promise = (async () => { try { @@ -125,13 +136,20 @@ export default class RequestClient { return fallbackAllowedHeaders } - this.uppy.log(`[CompanionClient] adding allowed preflight headers to companion cache: ${this.hostname} ${header}`) + this.uppy.log( + `[CompanionClient] adding allowed preflight headers to companion cache: ${this.hostname} ${header}`, + ) - const allowedHeaders = header.split(',').map((headerName) => headerName.trim().toLowerCase()) + const allowedHeaders = header + .split(',') + .map((headerName) => headerName.trim().toLowerCase()) allowedHeadersCache.set(this.hostname, allowedHeaders) return allowedHeaders } catch (err) { - this.uppy.log(`[CompanionClient] unable to make preflight request ${err}`, 'warning') + this.uppy.log( + `[CompanionClient] unable to make preflight request ${err}`, + 'warning', + ) // If the user gets a network error or similar, we should try preflight // again next time, or else we might get incorrect behaviour. allowedHeadersCache.delete(this.hostname) // re-fetch next time @@ -144,15 +162,22 @@ export default class RequestClient { } async preflightAndHeaders (path) { - const [allowedHeaders, headers] = await Promise.all([this.preflight(path), this.headers()]) + const [allowedHeaders, headers] = await Promise.all([ + this.preflight(path), + this.headers(), + ]) // filter to keep only allowed Headers - return Object.fromEntries(Object.entries(headers).filter(([header]) => { - if (!allowedHeaders.includes(header.toLowerCase())) { - this.uppy.log(`[CompanionClient] excluding disallowed header ${header}`) - return false - } - return true - })) + return Object.fromEntries( + Object.entries(headers).filter(([header]) => { + if (!allowedHeaders.includes(header.toLowerCase())) { + this.uppy.log( + `[CompanionClient] excluding disallowed header ${header}`, + ) + return false + } + return true + }), + ) } /** @protected */ @@ -170,7 +195,9 @@ export default class RequestClient { return handleJSONResponse(response) } catch (err) { if (err?.isAuthError) throw err - throw new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, { cause: err }) + throw new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, { + cause: err, + }) } } @@ -195,18 +222,26 @@ export default class RequestClient { return this.request({ ...options, path, method: 'DELETE', data }) } - async uploadRemoteFile (file, options = {}, requests) { + async uploadRemoteFile (file, reqBody, options = {}) { try { if (file.serverToken) { - return await this.connectToServerSocket(file, this.requests) + return await this.connectToServerSocket(file, this.getQueue()) } - const queueRequestSocketToken = requests.wrapPromiseFunction(this.#requestSocketToken, { priority: -1 }) - const serverToken = await queueRequestSocketToken(file).abortOn(options.signal) + const queueRequestSocketToken = this.getQueue().wrapPromiseFunction( + this.#requestSocketToken, + { priority: -1 }, + ) + const serverToken = await queueRequestSocketToken(file, reqBody).abortOn( + options.signal, + ) if (!this.uppy.getState().files[file.id]) return undefined this.uppy.setFileState(file.id, { serverToken }) - return await this.connectToServerSocket(this.uppy.getFile(file.id), requests) + return await this.connectToServerSocket( + this.uppy.getFile(file.id), + this.getQueue(), + ) } catch (err) { if (err?.cause?.name === 'AbortError') { // The file upload was aborted, it’s not an error @@ -219,35 +254,30 @@ export default class RequestClient { } } - #requestSocketToken = async (file, options) => { - const opts = { ...this.opts } - - if (file.tus) { - // Install file-specific upload overrides. - Object.assign(opts, file.tus) - } - + #requestSocketToken = async (file, postBody) => { if (file.remote.url == null) { throw new Error('Cannot connect to an undefined URL') } const res = await this.post(file.remote.url, { ...file.remote.body, - protocol: 's3-multipart', - size: file.data.size, - metadata: file.meta, - }, options) + ...postBody, + }) + return res.token } /** * @param {UppyFile} file */ - async connectToServerSocket (file, requests) { + async connectToServerSocket (file, queue) { return new Promise((resolve, reject) => { const token = file.serverToken const host = getSocketHost(file.remote.companionUrl) - const socket = new Socket({ target: `${host}/api/${token}`, autoOpen: false }) + const socket = new Socket({ + target: `${host}/api/${token}`, + autoOpen: false, + }) const eventManager = new EventManager(this.uppy) let queuedRequest @@ -267,7 +297,7 @@ export default class RequestClient { // Resuming an upload should be queued, else you could pause and then // resume a queued upload to make it skip the queue. queuedRequest.abort() - queuedRequest = requests.run(() => { + queuedRequest = queue.run(() => { socket.open() socket.send('resume', {}) @@ -294,7 +324,7 @@ export default class RequestClient { if (file.error) { socket.send('pause', {}) } - queuedRequest = requests.run(() => { + queuedRequest = queue.run(() => { socket.open() socket.send('resume', {}) @@ -325,7 +355,9 @@ export default class RequestClient { socket.on('error', (errData) => { const { message } = errData.error - const error = Object.assign(new Error(message), { cause: errData.error }) + const error = Object.assign(new Error(message), { + cause: errData.error, + }) // If the remote retry optimisation should not be used, // close the socket—this will tell companion to clear state and delete the file. @@ -354,7 +386,7 @@ export default class RequestClient { resolve() }) - queuedRequest = requests.run(() => { + queuedRequest = queue.run(() => { if (file.isPaused) { socket.send('pause', {}) } else { diff --git a/packages/@uppy/tus/src/index.js b/packages/@uppy/tus/src/index.js index fe535367df..22a6efc669 100644 --- a/packages/@uppy/tus/src/index.js +++ b/packages/@uppy/tus/src/index.js @@ -443,6 +443,25 @@ export default class Tus extends BasePlugin { } } + #getCompanionClientArgs (file) { + const opts = { ...this.opts } + + if (file.tus) { + // Install file-specific upload overrides. + Object.assign(opts, file.tus) + } + + return { + ...file.remote.body, + endpoint: opts.endpoint, + uploadUrl: opts.uploadUrl, + protocol: 'tus', + size: file.data.size, + headers: opts.headers, + metadata: file.meta, + } + } + /** * @param {(UppyFile | FailedUppyFile)[]} files */ @@ -458,7 +477,8 @@ export default class Tus extends BasePlugin { if (file.isRemote) { // TODO: why do we need to do this? why not always one or the other? const Client = file.remote.providerOptions.provider ? Provider : RequestClient - const client = new Client(this.uppy, file.remote.providerOptions) + const getQueue = () => this.requests + const client = new Client(this.uppy, file.remote.providerOptions, getQueue) const controller = new AbortController() const removedHandler = (removedFile) => { @@ -466,7 +486,11 @@ export default class Tus extends BasePlugin { } this.uppy.on('file-removed', removedHandler) - const uploadPromise = client.uploadRemoteFile(file, { signal: controller.signal }, this.requests) + const uploadPromise = client.uploadRemoteFile( + file, + this.#getCompanionClientArgs(file), + { signal: controller.signal }, + ) this.requests.wrapSyncFunction(() => { this.uppy.off('file-removed', removedHandler) diff --git a/packages/@uppy/xhr-upload/src/index.js b/packages/@uppy/xhr-upload/src/index.js index 4c15103759..7a09d4f402 100644 --- a/packages/@uppy/xhr-upload/src/index.js +++ b/packages/@uppy/xhr-upload/src/index.js @@ -441,6 +441,25 @@ export default class XHRUpload extends BasePlugin { }) } + #getCompanionClientArgs (file) { + const opts = this.getOptions(file) + const allowedMetaFields = Array.isArray(opts.allowedMetaFields) + ? opts.allowedMetaFields + // Send along all fields by default. + : Object.keys(file.meta) + return { + ...file.remote.body, + protocol: 'multipart', + endpoint: opts.endpoint, + size: file.data.size, + fieldname: opts.fieldName, + metadata: Object.fromEntries(allowedMetaFields.map(name => [name, file.meta[name]])), + httpMethod: opts.method, + useFormData: opts.formData, + headers: opts.headers, + } + } + async #uploadFiles (files) { await Promise.allSettled(files.map((file, i) => { const current = parseInt(i, 10) + 1 @@ -449,7 +468,8 @@ export default class XHRUpload extends BasePlugin { if (file.isRemote) { // TODO: why do we need to do this? why not always one or the other? const Client = file.remote.providerOptions.provider ? Provider : RequestClient - const client = new Client(this.uppy, file.remote.providerOptions) + const getQueue = () => this.requests + const client = new Client(this.uppy, file.remote.providerOptions, getQueue) const controller = new AbortController() const removedHandler = (removedFile) => { @@ -457,7 +477,11 @@ export default class XHRUpload extends BasePlugin { } this.uppy.on('file-removed', removedHandler) - const uploadPromise = client.uploadRemoteFile(file, { signal: controller.signal }, this.requests) + const uploadPromise = client.uploadRemoteFile( + file, + this.#getCompanionClientArgs(file), + { signal: controller.signal }, + ) this.requests.wrapSyncFunction(() => { this.uppy.off('file-removed', removedHandler)