Skip to content

Commit

Permalink
Inject correct companion POST args into companion-client
Browse files Browse the repository at this point in the history
  • Loading branch information
Murderlon committed Jul 25, 2023
1 parent e08eb7d commit baed2db
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 50 deletions.
19 changes: 17 additions & 2 deletions packages/@uppy/aws-s3-multipart/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()

Expand All @@ -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)
Expand Down
33 changes: 31 additions & 2 deletions packages/@uppy/aws-s3/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`)
Expand All @@ -256,15 +280,20 @@ 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) => {
if (removedFile.id === file.id) controller.abort()
}
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)
Expand Down
4 changes: 2 additions & 2 deletions packages/@uppy/companion-client/src/Provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
112 changes: 72 additions & 40 deletions packages/@uppy/companion-client/src/RequestClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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
}
Expand All @@ -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()
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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 */
Expand All @@ -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,
})
}
}

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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', {})

Expand All @@ -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', {})

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -354,7 +386,7 @@ export default class RequestClient {
resolve()
})

queuedRequest = requests.run(() => {
queuedRequest = queue.run(() => {
if (file.isPaused) {
socket.send('pause', {})
} else {
Expand Down
28 changes: 26 additions & 2 deletions packages/@uppy/tus/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -458,15 +477,20 @@ 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) => {
if (removedFile.id === file.id) controller.abort()
}
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)
Expand Down
Loading

0 comments on commit baed2db

Please sign in to comment.