diff --git a/docs/guides/migration-guides.md b/docs/guides/migration-guides.md index 4222cc9803..ba4dcfabc7 100644 --- a/docs/guides/migration-guides.md +++ b/docs/guides/migration-guides.md @@ -102,6 +102,22 @@ const { someThingMyBackendReturns } = uppy.getFile(id).response; about this in the [plugin docs](https://uppy.io/docs/aws-s3-multipart/#when-should-i-use-it). - Remove deprecated `prepareUploadParts` option. +- Companion’s options (`companionUrl`, `companionHeaders`, and + `companionCookieRules`) are renamed to more generic names (`endpoint`, + `headers`, and `cookieRules`). + + Using Companion with the `@uppy/aws-s3` plugin only makes sense if you already + need Companion for remote providers (such as Google Drive). When using your + own backend, you can let Uppy do all the heavy lifting on the client which it + would normally do for Companion, so you don’t have to implement that yourself. + + As long as you return the JSON for the expected endpoints (see our + [server example](https://github.com/transloadit/uppy/blob/main/examples/aws-nodejs/index.js)), + you only need to set `endpoint`. + + If you are using Companion, rename the options. If you have a lot of + client-side complexity (`createMultipartUpload`, `signPart`, etc), consider + letting Uppy do this for you. ### `@uppy/core` diff --git a/docs/uploader/aws-s3-multipart.mdx b/docs/uploader/aws-s3-multipart.mdx index 629838edc0..a21183ac72 100644 --- a/docs/uploader/aws-s3-multipart.mdx +++ b/docs/uploader/aws-s3-multipart.mdx @@ -166,7 +166,7 @@ import '@uppy/dashboard/dist/style.min.css'; const uppy = new Uppy() .use(Dashboard, { inline: true, target: 'body' }) .use(AwsS3, { - companionUrl: 'https://companion.uppy.io', + endpoint: 'https://companion.uppy.io', }); ``` @@ -215,22 +215,23 @@ uploaded. ::: -#### `companionUrl` +#### `endpoint` -URL to a [Companion](/docs/companion) instance (`string`, default: `null`). +URL to your backend or to [Companion](/docs/companion) (`string`, default: +`null`). -#### `companionHeaders` +#### `headers` -Custom headers that should be sent along to [Companion](/docs/companion) on -every request (`Object`, default: `{}`). +Custom headers that should be sent along to the [`endpoint`](#endpoint) on every +request (`Object`, default: `{}`). -#### `companionCookiesRule` +#### `cookiesRule` This option correlates to the [RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials) (`string`, default: `'same-origin'`). -This tells the plugin whether to send cookies to [Companion](/docs/companion). +This tells the plugin whether to send cookies to the [`endpoint`](#endpoint). #### `retryDelays` @@ -432,7 +433,7 @@ upload sources), you can pass a boolean: ```js uppy.use(AwsS3, { // This is an example using Companion: - companionUrl: 'http://companion.uppy.io', + endpoint: 'http://companion.uppy.io', getTemporarySecurityCredentials: true, shouldUseMultipart: (file) => file.size > 100 * 2 ** 20, }); diff --git a/e2e/clients/dashboard-aws-multipart/app.js b/e2e/clients/dashboard-aws-multipart/app.js index d4a3f9f015..8abb12ff92 100644 --- a/e2e/clients/dashboard-aws-multipart/app.js +++ b/e2e/clients/dashboard-aws-multipart/app.js @@ -9,7 +9,7 @@ const uppy = new Uppy() .use(Dashboard, { target: '#app', inline: true }) .use(AwsS3Multipart, { limit: 2, - companionUrl: process.env.VITE_COMPANION_URL, + endpoint: process.env.VITE_COMPANION_URL, shouldUseMultipart: true, }) diff --git a/e2e/clients/dashboard-aws/app.js b/e2e/clients/dashboard-aws/app.js index eeaa7effe6..2c23b9205b 100644 --- a/e2e/clients/dashboard-aws/app.js +++ b/e2e/clients/dashboard-aws/app.js @@ -9,7 +9,7 @@ const uppy = new Uppy() .use(Dashboard, { target: '#app', inline: true }) .use(AwsS3, { limit: 2, - companionUrl: process.env.VITE_COMPANION_URL, + endpoint: process.env.VITE_COMPANION_URL, shouldUseMultipart: false, }) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index 2f13cc770d..20f8a003b4 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -2,6 +2,7 @@ const path = require('node:path') const crypto = require('node:crypto') +const { existsSync } = require('node:fs') require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') }) const express = require('express') @@ -9,6 +10,7 @@ const express = require('express') const app = express() const port = process.env.PORT ?? 8080 +const accessControlAllowOrigin = '*' // You should define the actual domain(s) that are allowed to make requests. const bodyParser = require('body-parser') const { @@ -21,19 +23,14 @@ const { UploadPartCommand, } = require('@aws-sdk/client-s3') const { getSignedUrl } = require('@aws-sdk/s3-request-presigner') -const { - STSClient, - GetFederationTokenCommand, -} = require('@aws-sdk/client-sts') +const { STSClient, GetFederationTokenCommand } = require('@aws-sdk/client-sts') const policy = { Version: '2012-10-17', Statement: [ { Effect: 'Allow', - Action: [ - 's3:PutObject', - ], + Action: ['s3:PutObject'], Resource: [ `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}/*`, `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}`, @@ -54,10 +51,10 @@ let stsClient const expiresIn = 900 // Define how long until a S3 signature expires. -function getS3Client () { +function getS3Client() { s3Client ??= new S3Client({ region: process.env.COMPANION_AWS_REGION, - credentials : { + credentials: { accessKeyId: process.env.COMPANION_AWS_KEY, secretAccessKey: process.env.COMPANION_AWS_SECRET, }, @@ -65,10 +62,10 @@ function getS3Client () { return s3Client } -function getSTSClient () { +function getSTSClient() { stsClient ??= new STSClient({ region: process.env.COMPANION_AWS_REGION, - credentials : { + credentials: { accessKeyId: process.env.COMPANION_AWS_KEY, secretAccessKey: process.env.COMPANION_AWS_SECRET, }, @@ -78,53 +75,61 @@ function getSTSClient () { app.use(bodyParser.urlencoded({ extended: true }), bodyParser.json()) -app.get('/', (req, res) => { - const htmlPath = path.join(__dirname, 'public', 'index.html') - res.sendFile(htmlPath) -}) - -app.get('/drag', (req, res) => { - const htmlPath = path.join(__dirname, 'public', 'drag.html') - res.sendFile(htmlPath) +app.get('/s3/sts', (req, res, next) => { + // Before giving the STS token to the client, you should first check is they + // are authorized to perform that operation, and if the request is legit. + // For the sake of simplification, we skip that check in this example. + + getSTSClient() + .send( + new GetFederationTokenCommand({ + Name: '123user', + // The duration, in seconds, of the role session. The value specified + // can range from 900 seconds (15 minutes) up to the maximum session + // duration set for the role. + DurationSeconds: expiresIn, + Policy: JSON.stringify(policy), + }), + ) + .then((response) => { + // Test creating multipart upload from the server — it works + // createMultipartUploadYo(response) + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) + res.setHeader('Cache-Control', `public,max-age=${expiresIn}`) + res.json({ + credentials: response.Credentials, + bucket: process.env.COMPANION_AWS_BUCKET, + region: process.env.COMPANION_AWS_REGION, + }) + }, next) }) +const signOnServer = (req, res, next) => { + // Before giving the signature to the user, you should first check is they + // are authorized to perform that operation, and if the request is legit. + // For the sake of simplification, we skip that check in this example. -app.get('/sts', (req, res, next) => { - getSTSClient().send(new GetFederationTokenCommand({ - Name: '123user', - // The duration, in seconds, of the role session. The value specified - // can range from 900 seconds (15 minutes) up to the maximum session - // duration set for the role. - DurationSeconds: expiresIn, - Policy: JSON.stringify(policy), - })).then(response => { - // Test creating multipart upload from the server — it works - // createMultipartUploadYo(response) - res.setHeader('Access-Control-Allow-Origin', '*') - res.setHeader('Cache-Control', `public,max-age=${expiresIn}`) - res.json({ - credentials: response.Credentials, - bucket: process.env.COMPANION_AWS_BUCKET, - region: process.env.COMPANION_AWS_REGION, - }) - }, next) -}) -app.post('/sign-s3', (req, res, next) => { const Key = `${crypto.randomUUID()}-${req.body.filename}` const { contentType } = req.body - getSignedUrl(getS3Client(), new PutObjectCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key, - ContentType: contentType, - }), { expiresIn }).then((url) => { - res.setHeader('Access-Control-Allow-Origin', '*') + getSignedUrl( + getS3Client(), + new PutObjectCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key, + ContentType: contentType, + }), + { expiresIn }, + ).then((url) => { + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ url, method: 'PUT', }) res.end() }, next) -}) +} +app.get('/s3/params', signOnServer) +app.post('/s3/sign', signOnServer) // === === // You can remove those endpoints if you only want to support the non-multipart uploads. @@ -133,7 +138,9 @@ app.post('/s3/multipart', (req, res, next) => { const client = getS3Client() const { type, metadata, filename } = req.body if (typeof filename !== 'string') { - return res.status(400).json({ error: 's3: content filename must be a string' }) + return res + .status(400) + .json({ error: 's3: content filename must be a string' }) } if (typeof type !== 'string') { return res.status(400).json({ error: 's3: content type must be a string' }) @@ -154,7 +161,7 @@ app.post('/s3/multipart', (req, res, next) => { next(err) return } - res.setHeader('Access-Control-Allow-Origin', '*') + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ key: data.Key, uploadId: data.UploadId, @@ -162,7 +169,7 @@ app.post('/s3/multipart', (req, res, next) => { }) }) -function validatePartNumber (partNumber) { +function validatePartNumber(partNumber) { // eslint-disable-next-line no-param-reassign partNumber = Number(partNumber) return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000 @@ -172,20 +179,33 @@ app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => { const { key } = req.query if (!validatePartNumber(partNumber)) { - return res.status(400).json({ error: 's3: the part number must be an integer between 1 and 10000.' }) + return res + .status(400) + .json({ + error: 's3: the part number must be an integer between 1 and 10000.', + }) } if (typeof key !== 'string') { - return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + return res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) } - return getSignedUrl(getS3Client(), new UploadPartCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - PartNumber: partNumber, - Body: '', - }), { expiresIn }).then((url) => { - res.setHeader('Access-Control-Allow-Origin', '*') + return getSignedUrl( + getS3Client(), + new UploadPartCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + PartNumber: partNumber, + Body: '', + }), + { expiresIn }, + ).then((url) => { + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ url, expires: expiresIn }) }, next) }) @@ -196,39 +216,52 @@ app.get('/s3/multipart/:uploadId', (req, res, next) => { const { key } = req.query if (typeof key !== 'string') { - res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) return } const parts = [] - function listPartsPage (startAt) { - client.send(new ListPartsCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - PartNumberMarker: startAt, - }), (err, data) => { - if (err) { - next(err) - return - } - - parts.push(...data.Parts) - - if (data.IsTruncated) { - // Get the next page. - listPartsPage(data.NextPartNumberMarker) - } else { - res.json(parts) - } - }) + function listPartsPage(startAt) { + client.send( + new ListPartsCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + PartNumberMarker: startAt, + }), + (err, data) => { + if (err) { + next(err) + return + } + + parts.push(...data.Parts) + + if (data.IsTruncated) { + // Get the next page. + listPartsPage(data.NextPartNumberMarker) + } else { + res.json(parts) + } + }, + ) } listPartsPage(0) }) -function isValidPart (part) { - return part && typeof part === 'object' && Number(part.PartNumber) && typeof part.ETag === 'string' +function isValidPart(part) { + return ( + part && + typeof part === 'object' && + Number(part.PartNumber) && + typeof part.ETag === 'string' + ) } app.post('/s3/multipart/:uploadId/complete', (req, res, next) => { const client = getS3Client() @@ -237,29 +270,41 @@ app.post('/s3/multipart/:uploadId/complete', (req, res, next) => { const { parts } = req.body if (typeof key !== 'string') { - return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + return res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) } if (!Array.isArray(parts) || !parts.every(isValidPart)) { - return res.status(400).json({ error: 's3: `parts` must be an array of {ETag, PartNumber} objects.' }) + return res + .status(400) + .json({ + error: 's3: `parts` must be an array of {ETag, PartNumber} objects.', + }) } - return client.send(new CompleteMultipartUploadCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - MultipartUpload: { - Parts: parts, + return client.send( + new CompleteMultipartUploadCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + MultipartUpload: { + Parts: parts, + }, + }), + (err, data) => { + if (err) { + next(err) + return + } + res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) + res.json({ + location: data.Location, + }) }, - }), (err, data) => { - if (err) { - next(err) - return - } - res.setHeader('Access-Control-Allow-Origin', '*') - res.json({ - location: data.Location, - }) - }) + ) }) app.delete('/s3/multipart/:uploadId', (req, res, next) => { @@ -268,24 +313,89 @@ app.delete('/s3/multipart/:uploadId', (req, res, next) => { const { key } = req.query if (typeof key !== 'string') { - return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' }) + return res + .status(400) + .json({ + error: + 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"', + }) } - return client.send(new AbortMultipartUploadCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - }), (err) => { - if (err) { - next(err) - return - } - res.json({}) - }) + return client.send( + new AbortMultipartUploadCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + }), + (err) => { + if (err) { + next(err) + return + } + res.json({}) + }, + ) }) // === === +// === === + +app.get('/', (req, res) => { + res.setHeader('Content-Type', 'text/html') + const htmlPath = path.join(__dirname, 'public', 'index.html') + res.sendFile(htmlPath) +}) +app.get('/index.html', (req, res) => { + res.setHeader('Location', '/').sendStatus(308).end() +}) +app.get('/withCustomEndpoints.html', (req, res) => { + res.setHeader('Content-Type', 'text/html') + const htmlPath = path.join(__dirname, 'public', 'withCustomEndpoints.html') + res.sendFile(htmlPath) +}) + +app.get('/uppy.min.mjs', (req, res) => { + res.setHeader('Content-Type', 'text/javascript') + const bundlePath = path.join( + __dirname, + '../..', + 'packages/uppy/dist', + 'uppy.min.mjs', + ) + if (existsSync(bundlePath)) { + res.sendFile(bundlePath) + } else { + console.warn( + 'No local JS bundle found, using the CDN as a fallback. Run `corepack yarn build` to make this warning disappear.', + ) + res.end( + 'export * from "https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs";\n', + ) + } +}) +app.get('/uppy.min.css', (req, res) => { + res.setHeader('Content-Type', 'text/css') + const bundlePath = path.join( + __dirname, + '../..', + 'packages/uppy/dist', + 'uppy.min.css', + ) + if (existsSync(bundlePath)) { + res.sendFile(bundlePath) + } else { + console.warn( + 'No local CSS bundle found, using the CDN as a fallback. Run `corepack yarn build` to make this warning disappear.', + ) + res.end( + '@import "https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.css";\n', + ) + } +}) + app.listen(port, () => { - console.log(`Example app listening on port ${port}`) + console.log(`Example app listening on port ${port}.`) + console.log(`Visit http://localhost:${port}/ on your browser to try it.`) }) +// === === diff --git a/examples/aws-nodejs/public/drag.html b/examples/aws-nodejs/public/drag.html deleted file mode 100644 index 4fb3b96804..0000000000 --- a/examples/aws-nodejs/public/drag.html +++ /dev/null @@ -1,104 +0,0 @@ - - - - - Uppy - - - -
-
-
-
-
Uploaded files:
-
    -
    - -
    - - diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html index 3a5996ebb8..881a07567a 100644 --- a/examples/aws-nodejs/public/index.html +++ b/examples/aws-nodejs/public/index.html @@ -3,264 +3,68 @@ Uppy – AWS upload example - +

    AWS upload example

    -
    +
    + Sign on the server +
    +
    +
    + Sign on the client (if WebCrypto is available) +
    +
    + + diff --git a/examples/aws-nodejs/public/withCustomEndpoints.html b/examples/aws-nodejs/public/withCustomEndpoints.html new file mode 100644 index 0000000000..2265a7bb70 --- /dev/null +++ b/examples/aws-nodejs/public/withCustomEndpoints.html @@ -0,0 +1,267 @@ + + + + + Uppy – AWS upload example + + + +

    AWS upload example

    +
    + + + + + diff --git a/packages/@uppy/aws-s3/src/index.test.ts b/packages/@uppy/aws-s3/src/index.test.ts index 90c0ae8933..cce88b432f 100644 --- a/packages/@uppy/aws-s3/src/index.test.ts +++ b/packages/@uppy/aws-s3/src/index.test.ts @@ -29,7 +29,7 @@ describe('AwsS3Multipart', () => { core.use(AwsS3Multipart) const awsS3Multipart = core.getPlugin('AwsS3Multipart')! - const err = 'Expected a `companionUrl` option' + const err = 'Expected a `endpoint` option' const file = {} const opts = {} @@ -330,8 +330,8 @@ describe('AwsS3Multipart', () => { beforeEach(() => { core = new Core() core.use(AwsS3Multipart, { - companionUrl: '', - companionHeaders: { + endpoint: '', + headers: { authorization: oldToken, }, }) @@ -340,7 +340,8 @@ describe('AwsS3Multipart', () => { it('companionHeader is updated before uploading file', async () => { awsS3Multipart.setOptions({ - companionHeaders: { + endpoint: 'http://localhost', + headers: { authorization: newToken, }, }) @@ -371,7 +372,8 @@ describe('AwsS3Multipart', () => { Body > awsS3Multipart.setOptions({ - companionHeaders: { + endpoint: 'http://localhost', + headers: { authorization: newToken, }, }) diff --git a/packages/@uppy/aws-s3/src/index.ts b/packages/@uppy/aws-s3/src/index.ts index e642ed1f8c..3ae8c8bc40 100644 --- a/packages/@uppy/aws-s3/src/index.ts +++ b/packages/@uppy/aws-s3/src/index.ts @@ -143,9 +143,15 @@ export interface AwsS3Part { } type AWSS3WithCompanion = { - companionUrl: string - companionHeaders?: Record - companionCookiesRule?: string + endpoint: ConstructorParameters< + typeof RequestClient + >[1]['companionUrl'] + headers?: ConstructorParameters< + typeof RequestClient + >[1]['companionHeaders'] + cookiesRule?: ConstructorParameters< + typeof RequestClient + >[1]['companionCookiesRule'] getTemporarySecurityCredentials?: true } type AWSS3WithoutCompanion = { @@ -253,11 +259,7 @@ type AWSS3MaybeMultipartWithoutCompanion< shouldUseMultipart: (file: UppyFile) => boolean } -type RequestClientOptions = Partial< - ConstructorParameters>[1] -> - -interface _AwsS3MultipartOptions extends PluginOpts, RequestClientOptions { +interface _AwsS3MultipartOptions extends PluginOpts { allowedMetaFields?: string[] | boolean limit?: number retryDelays?: number[] | null @@ -285,7 +287,6 @@ const defaultOptions = { // eslint-disable-next-line no-bitwise (file.size! >> 10) >> 10 > 100) as any as true, retryDelays: [0, 1000, 3000, 5000], - companionHeaders: {}, } satisfies Partial> export default class AwsS3Multipart< @@ -303,6 +304,7 @@ export default class AwsS3Multipart< | 'completeMultipartUpload' > & Required> & + Partial & AWSS3MultipartWithoutCompanionMandatorySignPart & AWSS3NonMultipartWithoutCompanionMandatory, M, @@ -335,8 +337,7 @@ export default class AwsS3Multipart< // We need the `as any` here because of the dynamic default options. this.type = 'uploader' this.id = this.opts.id || 'AwsS3Multipart' - // TODO: only initiate `RequestClient` is `companionUrl` is defined. - this.#client = new RequestClient(uppy, (opts as any) ?? {}) + this.#setClient(opts) const dynamicDefaultOptions = { createMultipartUpload: this.createMultipartUpload, @@ -385,10 +386,59 @@ export default class AwsS3Multipart< return this.#client } + #setClient(opts?: Partial>) { + if ( + opts == null || + !( + 'endpoint' in opts || + 'companionUrl' in opts || + 'headers' in opts || + 'companionHeaders' in opts || + 'cookiesRule' in opts || + 'companionCookiesRule' in opts + ) + ) + return + if ('companionUrl' in opts && !('endpoint' in opts)) { + this.uppy.log( + '`companionUrl` option has been removed in @uppy/aws-s3, use `endpoint` instead.', + 'warning', + ) + } + if ('companionHeaders' in opts && !('headers' in opts)) { + this.uppy.log( + '`companionHeaders` option has been removed in @uppy/aws-s3, use `headers` instead.', + 'warning', + ) + } + if ('companionCookiesRule' in opts && !('cookiesRule' in opts)) { + this.uppy.log( + '`companionCookiesRule` option has been removed in @uppy/aws-s3, use `cookiesRule` instead.', + 'warning', + ) + } + if ('endpoint' in opts) { + this.#client = new RequestClient(this.uppy, { + pluginId: this.id, + provider: 'AWS', + companionUrl: this.opts.endpoint!, + companionHeaders: this.opts.headers, + companionCookiesRule: this.opts.cookiesRule, + }) + } else { + if ('headers' in opts) { + this.#setCompanionHeaders() + } + if ('cookiesRule' in opts) { + this.#client.opts.companionCookiesRule = opts.cookiesRule + } + } + } + setOptions(newOptions: Partial>): void { this.#companionCommunicationQueue.setOptions(newOptions) - super.setOptions(newOptions) - this.#setCompanionHeaders() + super.setOptions(newOptions as any) + this.#setClient(newOptions) } /** @@ -410,9 +460,9 @@ export default class AwsS3Multipart< } #assertHost(method: string): void { - if (!this.opts.companionUrl) { + if (!this.#client) { throw new Error( - `Expected a \`companionUrl\` option containing a Companion address, or if you are not using Companion, a custom \`${method}\` implementation.`, + `Expected a \`endpoint\` option containing a URL, or if you are not using Companion, a custom \`${method}\` implementation.`, ) } } @@ -486,15 +536,18 @@ export default class AwsS3Multipart< throwIfAborted(options?.signal) if (this.#cachedTemporaryCredentials == null) { + const { getTemporarySecurityCredentials } = this.opts // We do not await it just yet, so concurrent calls do not try to override it: - if (this.opts.getTemporarySecurityCredentials === true) { + if (getTemporarySecurityCredentials === true) { this.#assertHost('getTemporarySecurityCredentials') this.#cachedTemporaryCredentials = this.#client .get('s3/sts', options) .then(assertServerError) } else { this.#cachedTemporaryCredentials = - this.opts.getTemporarySecurityCredentials(options) + (getTemporarySecurityCredentials as AWSS3WithoutCompanion['getTemporarySecurityCredentials'])!( + options, + ) } this.#cachedTemporaryCredentials = await this.#cachedTemporaryCredentials setTimeout( @@ -572,7 +625,6 @@ export default class AwsS3Multipart< abortMultipartUpload( file: UppyFile, { key, uploadId, signal }: UploadResultWithSignal, - // eslint-disable-next-line @typescript-eslint/no-unused-vars ): Promise { this.#assertHost('abortMultipartUpload') @@ -920,7 +972,7 @@ export default class AwsS3Multipart< } #setCompanionHeaders = () => { - this.#client.setCompanionHeaders(this.opts.companionHeaders) + this.#client?.setCompanionHeaders(this.opts.headers!) } #setResumableUploadsCapability = (boolean: boolean) => {