From 60bfe980268c35edc23cfef1f90891243dabc4c3 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 20 Jun 2024 12:56:58 +0200 Subject: [PATCH 1/7] examples: add SHA256 checksum to uploads --- examples/aws-nodejs/index.js | 12 ++++- examples/aws-nodejs/public/index.html | 66 +++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index c0bf8744c1..188342c19f 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -109,7 +109,7 @@ const signOnServer = (req, res, next) => { // For the sake of simplification, we skip that check in this example. const Key = `${crypto.randomUUID()}-${req.body.filename}` - const { contentType } = req.body + const { contentType, ChecksumSHA256 } = req.body getSignedUrl( getS3Client(), @@ -117,6 +117,8 @@ const signOnServer = (req, res, next) => { Bucket: process.env.COMPANION_AWS_BUCKET, Key, ContentType: contentType, + ChecksumAlgorithm: 'SHA256', + ChecksumSHA256, }), { expiresIn }, ).then((url) => { @@ -352,6 +354,14 @@ app.get('/withCustomEndpoints.html', (req, res) => { res.sendFile(htmlPath) }) +app.get('/uppy.min.mjs.map', (req, res) => { + res.sendFile(path.join( + __dirname, + '../..', + 'packages/uppy/dist', + 'uppy.min.mjs.map', + )) +}) app.get('/uppy.min.mjs', (req, res) => { res.setHeader('Content-Type', 'text/javascript') const bundlePath = path.join( diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html index 881a07567a..33893061bd 100644 --- a/examples/aws-nodejs/public/index.html +++ b/examples/aws-nodejs/public/index.html @@ -37,6 +37,28 @@

AWS upload example

file.meta['name'], ) } + function* serializeSubPart(key, value) { + if (typeof value !== 'object') { + yield [key, value] + return + } + if (Array.isArray(value)) { + for (const val of value) { + yield* serializeSubPart(`${key}[]`, val) + } + return + } + for (const [subkey, val] of Object.entries(value)) { + yield* serializeSubPart(key ? `${key}[${subkey}]` : subkey, val) + } + } + function serialize(data) { + // If you want to avoid preflight requests, use URL-encoded syntax: + return new URLSearchParams(serializeSubPart(null, data)) + // If you don't care about additional preflight requests, you can also use: + // return JSON.stringify(data) + // You'd also have to add `Content-Type` header with value `application/json`. + } { const uppy = new Uppy() .use(Dashboard, { @@ -46,6 +68,50 @@

AWS upload example

.use(AwsS3, { id: 'myAWSPlugin', endpoint: '/', + async getUploadParameters(file, options) { + const arrayBuffer = await file.data.arrayBuffer() + const hashBuffer = await crypto.subtle.digest( + 'SHA-256', + arrayBuffer, + ) + const stringifiedHash = String.fromCharCode( + ...new Uint8Array(hashBuffer), + ) + const ChecksumSHA256 = window.btoa(stringifiedHash) + + // Send a request to our Express.js signing endpoint. + const response = await fetch('/s3/sign', { + method: 'POST', + headers: { + accept: 'application/json', + }, + body: serialize({ + filename: file.name, + contentType: file.type, + ChecksumSHA256, + }), + signal: options.signal, + }) + + if (!response.ok) + throw new Error('Unsuccessful request', { cause: response }) + + // Parse the JSON response. + const data = await response.json() + + // Return an object in the correct shape. + return { + method: data.method, + url: data.url, + fields: {}, // For presigned PUT uploads, this should be left empty. + // Provide content type header required by S3 + headers: { + 'Content-Type': file.type, + 'x-amz-checksum-sha256': ChecksumSHA256, + 'x-amz-sdk-checksum-algorithm': 'SHA256', + }, + } + }, }) uppy.on('complete', onUploadComplete) From 52adcd2c9e1b931114bd83b910fbd03ed76fbe6c Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 20 Jun 2024 14:12:04 +0200 Subject: [PATCH 2/7] Fix signing --- examples/aws-nodejs/index.js | 112 +++++++++++++++++------------------ 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index 188342c19f..f8a69b4196 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -120,7 +120,15 @@ const signOnServer = (req, res, next) => { ChecksumAlgorithm: 'SHA256', ChecksumSHA256, }), - { expiresIn }, + { + expiresIn, + // IMPORTANT: the SDK strips x-amz headers by default + // but we need the checksum headers + unhoistableHeaders: new Set([ + 'x-amz-sdk-checksum-algorithm', + 'x-amz-checksum-sha256', + ]), + }, ).then((url) => { res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) res.json({ @@ -181,19 +189,15 @@ 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( @@ -218,38 +222,39 @@ 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(startsAt = undefined) { - client.send(new ListPartsCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - PartNumberMarker: startsAt, - }), (err, data) => { - if (err) { - next(err) - return - } + client.send( + new ListPartsCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + PartNumberMarker: startsAt, + }), + (err, data) => { + if (err) { + next(err) + return + } parts.push(...data.Parts) - // continue to get list of all uploaded parts until the IsTruncated flag is false - if (data.IsTruncated) { - listPartsPage(data.NextPartNumberMarker) - } else { - res.json(parts) - } - }) + // continue to get list of all uploaded parts until the IsTruncated flag is false + if (data.IsTruncated) { + listPartsPage(data.NextPartNumberMarker) + } else { + res.json(parts) + } + }, + ) } listPartsPage() }) @@ -269,19 +274,15 @@ 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( @@ -312,12 +313,10 @@ 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( @@ -355,12 +354,9 @@ app.get('/withCustomEndpoints.html', (req, res) => { }) app.get('/uppy.min.mjs.map', (req, res) => { - res.sendFile(path.join( - __dirname, - '../..', - 'packages/uppy/dist', - 'uppy.min.mjs.map', - )) + res.sendFile( + path.join(__dirname, '../..', 'packages/uppy/dist', 'uppy.min.mjs.map'), + ) }) app.get('/uppy.min.mjs', (req, res) => { res.setHeader('Content-Type', 'text/javascript') From b63b15239dc0df320a811747c12e0720e92aefe2 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 20 Jun 2024 14:13:53 +0200 Subject: [PATCH 3/7] Correct comment --- examples/aws-nodejs/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index f8a69b4196..f35d754d2b 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -122,8 +122,9 @@ const signOnServer = (req, res, next) => { }), { expiresIn, - // IMPORTANT: the SDK strips x-amz headers by default - // but we need the checksum headers + // If not supplied, the presigner moves all the AWS-specific headers + // (starting with `x-amz-`) to the request query string. + // If supplied, these headers remain in the presigned request's header. unhoistableHeaders: new Set([ 'x-amz-sdk-checksum-algorithm', 'x-amz-checksum-sha256', From 1b1418c1c23dda1c42a9984794d20b3e706b7b43 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 20 Jun 2024 14:19:28 +0200 Subject: [PATCH 4/7] remove formatting changes --- examples/aws-nodejs/index.js | 98 +++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 47 deletions(-) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index f35d754d2b..8309f4d0ac 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -190,15 +190,19 @@ 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( @@ -223,39 +227,38 @@ 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(startsAt = undefined) { - client.send( - new ListPartsCommand({ - Bucket: process.env.COMPANION_AWS_BUCKET, - Key: key, - UploadId: uploadId, - PartNumberMarker: startsAt, - }), - (err, data) => { - if (err) { - next(err) - return - } + client.send(new ListPartsCommand({ + Bucket: process.env.COMPANION_AWS_BUCKET, + Key: key, + UploadId: uploadId, + PartNumberMarker: startsAt, + }), (err, data) => { + if (err) { + next(err) + return + } parts.push(...data.Parts) - // continue to get list of all uploaded parts until the IsTruncated flag is false - if (data.IsTruncated) { - listPartsPage(data.NextPartNumberMarker) - } else { - res.json(parts) - } - }, - ) + // continue to get list of all uploaded parts until the IsTruncated flag is false + if (data.IsTruncated) { + listPartsPage(data.NextPartNumberMarker) + } else { + res.json(parts) + } + }) } listPartsPage() }) @@ -275,15 +278,19 @@ 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( @@ -314,10 +321,12 @@ 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( @@ -354,11 +363,6 @@ app.get('/withCustomEndpoints.html', (req, res) => { res.sendFile(htmlPath) }) -app.get('/uppy.min.mjs.map', (req, res) => { - res.sendFile( - path.join(__dirname, '../..', 'packages/uppy/dist', 'uppy.min.mjs.map'), - ) -}) app.get('/uppy.min.mjs', (req, res) => { res.setHeader('Content-Type', 'text/javascript') const bundlePath = path.join( From b5f31484290c04784028fa2c4f1b02b29b84dabc Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 20 Jun 2024 16:45:51 +0200 Subject: [PATCH 5/7] with multipart --- examples/aws-nodejs/index.js | 34 ++++++++--- examples/aws-nodejs/public/index.html | 44 --------------- .../public/withCustomEndpoints.html | 56 +++++++++---------- 3 files changed, 52 insertions(+), 82 deletions(-) diff --git a/examples/aws-nodejs/index.js b/examples/aws-nodejs/index.js index 8309f4d0ac..fe8054fcf7 100644 --- a/examples/aws-nodejs/index.js +++ b/examples/aws-nodejs/index.js @@ -13,6 +13,11 @@ 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 unhoistableHeaders = new Set([ + 'x-amz-sdk-checksum-algorithm', + 'x-amz-checksum-sha256', +]) + const { S3Client, AbortMultipartUploadCommand, @@ -125,10 +130,7 @@ const signOnServer = (req, res, next) => { // If not supplied, the presigner moves all the AWS-specific headers // (starting with `x-amz-`) to the request query string. // If supplied, these headers remain in the presigned request's header. - unhoistableHeaders: new Set([ - 'x-amz-sdk-checksum-algorithm', - 'x-amz-checksum-sha256', - ]), + unhoistableHeaders, }, ).then((url) => { res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) @@ -163,6 +165,7 @@ app.post('/s3/multipart', (req, res, next) => { Key, ContentType: type, Metadata: metadata, + ChecksumAlgorithm: 'SHA256', } const command = new CreateMultipartUploadCommand(params) @@ -187,7 +190,7 @@ function validatePartNumber(partNumber) { } app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => { const { uploadId, partNumber } = req.params - const { key } = req.query + const { key, sha256 } = req.query if (!validatePartNumber(partNumber)) { return res @@ -212,12 +215,20 @@ app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => { Key: key, UploadId: uploadId, PartNumber: partNumber, - Body: '', + ChecksumSHA256: sha256, }), - { expiresIn }, + { + expiresIn, + // If not supplied, the presigner moves all the AWS-specific headers + // (starting with `x-amz-`) to the request query string. + // If supplied, these headers remain in the presigned request's header. + unhoistableHeaders, + }, ).then((url) => { res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin) - res.json({ url, expires: expiresIn }) + res.json({ url, expires: expiresIn, headers: { + 'x-amz-checksum-sha256': sha256, + } }) }, next) }) @@ -275,7 +286,12 @@ app.post('/s3/multipart/:uploadId/complete', (req, res, next) => { const client = getS3Client() const { uploadId } = req.params const { key } = req.query - const { parts } = req.body + + const parts = Array.from(req.body.parts[0].ETag, (ETag, i) => ({ + ETag, + PartNumber: req.body.parts[0].PartNumber[i], + ChecksumSHA256: req.body.parts[0].ChecksumSHA256[i], + })) if (typeof key !== 'string') { return res diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html index 33893061bd..10dafa6d95 100644 --- a/examples/aws-nodejs/public/index.html +++ b/examples/aws-nodejs/public/index.html @@ -68,50 +68,6 @@

AWS upload example

.use(AwsS3, { id: 'myAWSPlugin', endpoint: '/', - async getUploadParameters(file, options) { - const arrayBuffer = await file.data.arrayBuffer() - const hashBuffer = await crypto.subtle.digest( - 'SHA-256', - arrayBuffer, - ) - const stringifiedHash = String.fromCharCode( - ...new Uint8Array(hashBuffer), - ) - const ChecksumSHA256 = window.btoa(stringifiedHash) - - // Send a request to our Express.js signing endpoint. - const response = await fetch('/s3/sign', { - method: 'POST', - headers: { - accept: 'application/json', - }, - body: serialize({ - filename: file.name, - contentType: file.type, - ChecksumSHA256, - }), - signal: options.signal, - }) - - if (!response.ok) - throw new Error('Unsuccessful request', { cause: response }) - - // Parse the JSON response. - const data = await response.json() - - // Return an object in the correct shape. - return { - method: data.method, - url: data.url, - fields: {}, // For presigned PUT uploads, this should be left empty. - // Provide content type header required by S3 - headers: { - 'Content-Type': file.type, - 'x-amz-checksum-sha256': ChecksumSHA256, - 'x-amz-sdk-checksum-algorithm': 'SHA256', - }, - } - }, }) uppy.on('complete', onUploadComplete) diff --git a/examples/aws-nodejs/public/withCustomEndpoints.html b/examples/aws-nodejs/public/withCustomEndpoints.html index 2265a7bb70..de8d125995 100644 --- a/examples/aws-nodejs/public/withCustomEndpoints.html +++ b/examples/aws-nodejs/public/withCustomEndpoints.html @@ -43,7 +43,18 @@

AWS upload example

// return JSON.stringify(data) // You'd also have to add `Content-Type` header with value `application/json`. } + async function computeHash(blob) { + const hashBuffer = await crypto.subtle.digest( + 'SHA-256', + await blob.arrayBuffer(), + ) + const stringifiedHash = String.fromCharCode( + ...new Uint8Array(hashBuffer), + ) + return btoa(stringifiedHash) + } { + const cache = Object.create(null) const MiB = 0x10_00_00 const uppy = new Uppy() @@ -55,18 +66,7 @@

AWS upload example

id: 'myAWSPlugin', // Files that are more than 100MiB should be uploaded in multiple parts. - shouldUseMultipart: (file) => file.size > 100 * MiB, - - /** - * This method tells Uppy how to retrieve a temporary token for signing on the client. - * Signing on the client is optional, you can also do the signing from the server. - */ - async getTemporarySecurityCredentials({ signal }) { - const response = await fetch('/s3/sts', { signal }) - if (!response.ok) - throw new Error('Unsuccessful request', { cause: response }) - return response.json() - }, + shouldUseMultipart: true, // ========== Non-Multipart Uploads ========== @@ -76,13 +76,7 @@

AWS upload example

* you don't need to implement it. */ async getUploadParameters(file, options) { - if (typeof crypto?.subtle === 'object') { - // If WebCrypto is available, let's do signing from the client. - return uppy - .getPlugin('myAWSPlugin') - .createSignedURL(file, options) - } - + const ChecksumSHA256 = await computeHash(file.data) // Send a request to our Express.js signing endpoint. const response = await fetch('/s3/sign', { method: 'POST', @@ -92,6 +86,7 @@

AWS upload example

body: serialize({ filename: file.name, contentType: file.type, + ChecksumSHA256, }), signal: options.signal, }) @@ -110,6 +105,8 @@

AWS upload example

// Provide content type header required by S3 headers: { 'Content-Type': file.type, + 'x-amz-checksum-sha256': ChecksumSHA256, + 'x-amz-sdk-checksum-algorithm': 'SHA256', }, } }, @@ -170,14 +167,7 @@

AWS upload example

}, async signPart(file, options) { - if (typeof crypto?.subtle === 'object') { - // If WebCrypto, let's do signing from the client. - return uppy - .getPlugin('myAWSPlugin') - .createSignedURL(file, options) - } - - const { uploadId, key, partNumber, signal } = options + const { uploadId, key, partNumber, signal, body } = options signal?.throwIfAborted() @@ -186,10 +176,12 @@

AWS upload example

'Cannot sign without a key, an uploadId, and a partNumber', ) } + const sha256 = await computeHash(body) + cache[uploadId + partNumber] = sha256 const filename = encodeURIComponent(key) const response = await fetch( - `/s3/multipart/${uploadId}/${partNumber}?key=${filename}`, + `/s3/multipart/${uploadId}/${partNumber}?key=${filename}&sha256=${encodeURIComponent(sha256)}`, { signal }, ) @@ -234,7 +226,13 @@

AWS upload example

headers: { accept: 'application/json', }, - body: serialize({ parts }), + body: serialize({ + parts: parts.map(({ ETag, PartNumber }) => { + const ChecksumSHA256 = cache[uploadId + PartNumber] + delete cache[uploadId + PartNumber] + return { ETag, PartNumber, ChecksumSHA256 } + }), + }), signal, }, ) From 12b6576d4a52dc7ec3f44addf4b5af02d7c30849 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 20 Jun 2024 16:46:46 +0200 Subject: [PATCH 6/7] remove unrelated change --- examples/aws-nodejs/public/index.html | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/examples/aws-nodejs/public/index.html b/examples/aws-nodejs/public/index.html index 10dafa6d95..881a07567a 100644 --- a/examples/aws-nodejs/public/index.html +++ b/examples/aws-nodejs/public/index.html @@ -37,28 +37,6 @@

AWS upload example

file.meta['name'], ) } - function* serializeSubPart(key, value) { - if (typeof value !== 'object') { - yield [key, value] - return - } - if (Array.isArray(value)) { - for (const val of value) { - yield* serializeSubPart(`${key}[]`, val) - } - return - } - for (const [subkey, val] of Object.entries(value)) { - yield* serializeSubPart(key ? `${key}[${subkey}]` : subkey, val) - } - } - function serialize(data) { - // If you want to avoid preflight requests, use URL-encoded syntax: - return new URLSearchParams(serializeSubPart(null, data)) - // If you don't care about additional preflight requests, you can also use: - // return JSON.stringify(data) - // You'd also have to add `Content-Type` header with value `application/json`. - } { const uppy = new Uppy() .use(Dashboard, { From a5138ac0a1b9c194c5b864b6e0b555dbab0072d5 Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Thu, 20 Jun 2024 16:48:02 +0200 Subject: [PATCH 7/7] Update examples/aws-nodejs/public/withCustomEndpoints.html --- examples/aws-nodejs/public/withCustomEndpoints.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/aws-nodejs/public/withCustomEndpoints.html b/examples/aws-nodejs/public/withCustomEndpoints.html index de8d125995..f2f1f8c8fb 100644 --- a/examples/aws-nodejs/public/withCustomEndpoints.html +++ b/examples/aws-nodejs/public/withCustomEndpoints.html @@ -66,7 +66,7 @@

AWS upload example

id: 'myAWSPlugin', // Files that are more than 100MiB should be uploaded in multiple parts. - shouldUseMultipart: true, + shouldUseMultipart: (file) => file.size > 100 * MiB, // ========== Non-Multipart Uploads ==========