mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
@uppy/aws-s3: add endpoint option (#5173)
This commit is contained in:
parent
9421cea0fb
commit
087d8f07f0
10 changed files with 642 additions and 494 deletions
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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('/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.
|
||||
|
||||
app.get('/drag', (req, res) => {
|
||||
const htmlPath = path.join(__dirname, 'public', 'drag.html')
|
||||
res.sendFile(htmlPath)
|
||||
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)
|
||||
|
||||
// === <S3 Multipart> ===
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
parts.push(...data.Parts)
|
||||
|
||||
if (data.IsTruncated) {
|
||||
// Get the next page.
|
||||
listPartsPage(data.NextPartNumberMarker)
|
||||
} else {
|
||||
res.json(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({})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// === </S3 MULTIPART> ===
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Example app listening on port ${port}`)
|
||||
// === <some plumbing to make the example work> ===
|
||||
|
||||
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(`Visit http://localhost:${port}/ on your browser to try it.`)
|
||||
})
|
||||
// === </some plumbing to make the example work> ===
|
||||
|
|
|
|||
|
|
@ -1,104 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy</title>
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<section class="example">
|
||||
<div id="drag-drop-area"></div>
|
||||
<div class="for-ProgressBar"></div>
|
||||
<div class="uploaded-files">
|
||||
<h5>Uploaded files:</h5>
|
||||
<ol></ol>
|
||||
</div>
|
||||
<script type="module">
|
||||
import {
|
||||
Uppy,
|
||||
DragDrop,
|
||||
ProgressBar,
|
||||
AwsS3,
|
||||
} from 'https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs'
|
||||
|
||||
// Function for displaying uploaded files
|
||||
const onUploadSuccess = (elForUploadedFiles) => (file, response) => {
|
||||
const url = response.uploadURL
|
||||
const fileName = file.name
|
||||
|
||||
const li = document.createElement('li')
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.target = '_blank'
|
||||
a.appendChild(document.createTextNode(fileName))
|
||||
li.appendChild(a)
|
||||
|
||||
document.querySelector(elForUploadedFiles).appendChild(li)
|
||||
}
|
||||
|
||||
var uppy = new Uppy({
|
||||
autoProceed: true,
|
||||
restrictions: {
|
||||
maxNumberOfFiles: 10,
|
||||
},
|
||||
})
|
||||
.use(DragDrop, {
|
||||
inline: true,
|
||||
target: '#drag-drop-area',
|
||||
})
|
||||
.use(ProgressBar, {
|
||||
target: '.example .for-ProgressBar',
|
||||
hideAfterFinish: true,
|
||||
})
|
||||
.use(AwsS3, {
|
||||
getUploadParameters(file) {
|
||||
// Send a request to our PHP signing endpoint.
|
||||
return fetch('/sign-s3', {
|
||||
method: 'post',
|
||||
// Send and receive JSON.
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
filename: file.name,
|
||||
contentType: file.type,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
// Parse the JSON response.
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => {
|
||||
// Return an object in the correct shape.
|
||||
return {
|
||||
method: data.method,
|
||||
url: data.url,
|
||||
fields: data.fields,
|
||||
// Provide content type header required by S3
|
||||
headers: {
|
||||
'Content-Type': file.type,
|
||||
},
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
uppy.on('complete', (result) => {
|
||||
console.log(
|
||||
'Upload complete! We’ve uploaded these files:',
|
||||
result.successful,
|
||||
)
|
||||
})
|
||||
|
||||
uppy.on(
|
||||
'upload-success',
|
||||
onUploadSuccess('.example .uploaded-files ol'),
|
||||
)
|
||||
</script>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,264 +3,68 @@
|
|||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS upload example</title>
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link href="./uppy.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>AWS upload example</h1>
|
||||
<div id="uppy"></div>
|
||||
<details open name="uppy">
|
||||
<summary>Sign on the server</summary>
|
||||
<div id="uppy-sign-on-server"></div>
|
||||
</details>
|
||||
<details name="uppy">
|
||||
<summary>Sign on the client (if WebCrypto is available)</summary>
|
||||
<div id="uppy-sign-on-client"></div>
|
||||
</details>
|
||||
<footer>
|
||||
You seeing the simplified example, with a backend that mimicks a
|
||||
Companion-like instance. See
|
||||
<a href="./withCustomEndpoints.html">the custom endpoint example</a> if
|
||||
you need to see how to use one or more custom function for handling
|
||||
communication with the backend.
|
||||
</footer>
|
||||
<noscript>You need JavaScript to run this example.</noscript>
|
||||
<script type="module">
|
||||
import {
|
||||
Uppy,
|
||||
Dashboard,
|
||||
AwsS3,
|
||||
} from 'https://releases.transloadit.com/uppy/v4.0.0-beta.11/uppy.min.mjs'
|
||||
/**
|
||||
* This generator transforms a deep object into URL-encodable pairs
|
||||
* to work with `URLSearchParams` on the client and `body-parser` on the server.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
||||
function onUploadComplete(result) {
|
||||
console.log(
|
||||
'Upload complete! We’ve uploaded these files:',
|
||||
result.successful,
|
||||
)
|
||||
}
|
||||
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`.
|
||||
function onUploadSuccess(file, data) {
|
||||
console.log(
|
||||
'Upload success! We’ve uploaded this file:',
|
||||
file.meta['name'],
|
||||
)
|
||||
}
|
||||
{
|
||||
const MiB = 0x10_00_00
|
||||
|
||||
const uppy = new Uppy()
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy',
|
||||
target: '#uppy-sign-on-server',
|
||||
})
|
||||
.use(AwsS3, {
|
||||
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('/sts', { signal })
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
return response.json()
|
||||
},
|
||||
|
||||
// ========== Non-Multipart Uploads ==========
|
||||
|
||||
/**
|
||||
* This method tells Uppy how to handle non-multipart uploads.
|
||||
* If for some reason you want to only support multipart uploads,
|
||||
* 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)
|
||||
}
|
||||
|
||||
// Send a request to our Express.js signing endpoint.
|
||||
const response = await fetch('/sign-s3', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: serialize({
|
||||
filename: file.name,
|
||||
contentType: file.type,
|
||||
}),
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// ========== Multipart Uploads ==========
|
||||
|
||||
// The following methods are only useful for multipart uploads:
|
||||
// If you are not interested in multipart uploads, you don't need to
|
||||
// implement them (you'd also need to set `shouldUseMultipart: false` though).
|
||||
|
||||
async createMultipartUpload(file, signal) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
const metadata = {}
|
||||
|
||||
Object.keys(file.meta || {}).forEach((key) => {
|
||||
if (file.meta[key] != null) {
|
||||
metadata[key] = file.meta[key].toString()
|
||||
}
|
||||
})
|
||||
|
||||
const response = await fetch('/s3/multipart', {
|
||||
method: 'POST',
|
||||
// Send and receive JSON.
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: serialize({
|
||||
filename: file.name,
|
||||
type: file.type,
|
||||
metadata,
|
||||
}),
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
// Parse the JSON response.
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
async abortMultipartUpload(file, { key, uploadId, signal }) {
|
||||
const filename = encodeURIComponent(key)
|
||||
const uploadIdEnc = encodeURIComponent(uploadId)
|
||||
const response = await fetch(
|
||||
`/s3/multipart/${uploadIdEnc}?key=${filename}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
signal?.throwIfAborted()
|
||||
|
||||
if (uploadId == null || key == null || partNumber == null) {
|
||||
throw new Error(
|
||||
'Cannot sign without a key, an uploadId, and a partNumber',
|
||||
)
|
||||
}
|
||||
|
||||
const filename = encodeURIComponent(key)
|
||||
const response = await fetch(
|
||||
`/s3/multipart/${uploadId}/${partNumber}?key=${filename}`,
|
||||
{ signal },
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
async listParts(file, { key, uploadId }, signal) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
const filename = encodeURIComponent(key)
|
||||
const response = await fetch(
|
||||
`/s3/multipart/${uploadId}?key=${filename}`,
|
||||
{ signal },
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
async completeMultipartUpload(
|
||||
file,
|
||||
{ key, uploadId, parts },
|
||||
signal,
|
||||
) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
const filename = encodeURIComponent(key)
|
||||
const uploadIdEnc = encodeURIComponent(uploadId)
|
||||
const response = await fetch(
|
||||
`s3/multipart/${uploadIdEnc}/complete?key=${filename}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: serialize({ parts }),
|
||||
signal,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
endpoint: '/',
|
||||
})
|
||||
|
||||
uppy.on('complete', (result) => {
|
||||
console.log(
|
||||
'Upload complete! We’ve uploaded these files:',
|
||||
result.successful,
|
||||
)
|
||||
})
|
||||
uppy.on('complete', onUploadComplete)
|
||||
uppy.on('upload-success', onUploadSuccess)
|
||||
}
|
||||
{
|
||||
const uppy = new Uppy()
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy-sign-on-client',
|
||||
})
|
||||
.use(AwsS3, {
|
||||
id: 'myAWSPlugin',
|
||||
endpoint: '/',
|
||||
getTemporarySecurityCredentials: typeof crypto?.subtle === 'object',
|
||||
})
|
||||
|
||||
uppy.on('upload-success', (file, data) => {
|
||||
console.log(
|
||||
'Upload success! We’ve uploaded this file:',
|
||||
file.meta['name'],
|
||||
)
|
||||
})
|
||||
uppy.on('complete', onUploadComplete)
|
||||
uppy.on('upload-success', onUploadSuccess)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
267
examples/aws-nodejs/public/withCustomEndpoints.html
Normal file
267
examples/aws-nodejs/public/withCustomEndpoints.html
Normal file
|
|
@ -0,0 +1,267 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS upload example</title>
|
||||
<link href="./uppy.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<h1>AWS upload example</h1>
|
||||
<div id="uppy"></div>
|
||||
<footer>
|
||||
You seeing the complex example, with a backend that does not mimick a
|
||||
Companion-like instance. See
|
||||
<a href="./index.html">the simplified example</a> if you don't need custom
|
||||
functions for handling communication with the backend.
|
||||
</footer>
|
||||
<noscript>You need JavaScript to run this example.</noscript>
|
||||
<script type="module">
|
||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
||||
/**
|
||||
* This generator transforms a deep object into URL-encodable pairs
|
||||
* to work with `URLSearchParams` on the client and `body-parser` on the server.
|
||||
*/
|
||||
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 MiB = 0x10_00_00
|
||||
|
||||
const uppy = new Uppy()
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy',
|
||||
})
|
||||
.use(AwsS3, {
|
||||
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()
|
||||
},
|
||||
|
||||
// ========== Non-Multipart Uploads ==========
|
||||
|
||||
/**
|
||||
* This method tells Uppy how to handle non-multipart uploads.
|
||||
* If for some reason you want to only support multipart uploads,
|
||||
* 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)
|
||||
}
|
||||
|
||||
// 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,
|
||||
}),
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
// ========== Multipart Uploads ==========
|
||||
|
||||
// The following methods are only useful for multipart uploads:
|
||||
// If you are not interested in multipart uploads, you don't need to
|
||||
// implement them (you'd also need to set `shouldUseMultipart: false` though).
|
||||
|
||||
async createMultipartUpload(file, signal) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
const metadata = {}
|
||||
|
||||
Object.keys(file.meta || {}).forEach((key) => {
|
||||
if (file.meta[key] != null) {
|
||||
metadata[key] = file.meta[key].toString()
|
||||
}
|
||||
})
|
||||
|
||||
const response = await fetch('/s3/multipart', {
|
||||
method: 'POST',
|
||||
// Send and receive JSON.
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: serialize({
|
||||
filename: file.name,
|
||||
type: file.type,
|
||||
metadata,
|
||||
}),
|
||||
signal,
|
||||
})
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
// Parse the JSON response.
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
async abortMultipartUpload(file, { key, uploadId, signal }) {
|
||||
const filename = encodeURIComponent(key)
|
||||
const uploadIdEnc = encodeURIComponent(uploadId)
|
||||
const response = await fetch(
|
||||
`/s3/multipart/${uploadIdEnc}?key=${filename}`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
signal,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
},
|
||||
|
||||
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
|
||||
|
||||
signal?.throwIfAborted()
|
||||
|
||||
if (uploadId == null || key == null || partNumber == null) {
|
||||
throw new Error(
|
||||
'Cannot sign without a key, an uploadId, and a partNumber',
|
||||
)
|
||||
}
|
||||
|
||||
const filename = encodeURIComponent(key)
|
||||
const response = await fetch(
|
||||
`/s3/multipart/${uploadId}/${partNumber}?key=${filename}`,
|
||||
{ signal },
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
async listParts(file, { key, uploadId }, signal) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
const filename = encodeURIComponent(key)
|
||||
const response = await fetch(
|
||||
`/s3/multipart/${uploadId}?key=${filename}`,
|
||||
{ signal },
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
|
||||
async completeMultipartUpload(
|
||||
file,
|
||||
{ key, uploadId, parts },
|
||||
signal,
|
||||
) {
|
||||
signal?.throwIfAborted()
|
||||
|
||||
const filename = encodeURIComponent(key)
|
||||
const uploadIdEnc = encodeURIComponent(uploadId)
|
||||
const response = await fetch(
|
||||
`s3/multipart/${uploadIdEnc}/complete?key=${filename}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
},
|
||||
body: serialize({ parts }),
|
||||
signal,
|
||||
},
|
||||
)
|
||||
|
||||
if (!response.ok)
|
||||
throw new Error('Unsuccessful request', { cause: response })
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
return data
|
||||
},
|
||||
})
|
||||
|
||||
uppy.on('complete', (result) => {
|
||||
console.log(
|
||||
'Upload complete! We’ve uploaded these files:',
|
||||
result.successful,
|
||||
)
|
||||
})
|
||||
|
||||
uppy.on('upload-success', (file, data) => {
|
||||
console.log(
|
||||
'Upload success! We’ve uploaded this file:',
|
||||
file.meta['name'],
|
||||
)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -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<any, Body>()
|
||||
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,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -143,9 +143,15 @@ export interface AwsS3Part {
|
|||
}
|
||||
|
||||
type AWSS3WithCompanion = {
|
||||
companionUrl: string
|
||||
companionHeaders?: Record<string, string>
|
||||
companionCookiesRule?: string
|
||||
endpoint: ConstructorParameters<
|
||||
typeof RequestClient<any, any>
|
||||
>[1]['companionUrl']
|
||||
headers?: ConstructorParameters<
|
||||
typeof RequestClient<any, any>
|
||||
>[1]['companionHeaders']
|
||||
cookiesRule?: ConstructorParameters<
|
||||
typeof RequestClient<any, any>
|
||||
>[1]['companionCookiesRule']
|
||||
getTemporarySecurityCredentials?: true
|
||||
}
|
||||
type AWSS3WithoutCompanion = {
|
||||
|
|
@ -253,11 +259,7 @@ type AWSS3MaybeMultipartWithoutCompanion<
|
|||
shouldUseMultipart: (file: UppyFile<M, B>) => boolean
|
||||
}
|
||||
|
||||
type RequestClientOptions = Partial<
|
||||
ConstructorParameters<typeof RequestClient<any, any>>[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<AwsS3MultipartOptions<any, any>>
|
||||
|
||||
export default class AwsS3Multipart<
|
||||
|
|
@ -303,6 +304,7 @@ export default class AwsS3Multipart<
|
|||
| 'completeMultipartUpload'
|
||||
> &
|
||||
Required<Pick<AWSS3WithoutCompanion, 'uploadPartBytes'>> &
|
||||
Partial<AWSS3WithCompanion> &
|
||||
AWSS3MultipartWithoutCompanionMandatorySignPart<M, B> &
|
||||
AWSS3NonMultipartWithoutCompanionMandatory<M, B>,
|
||||
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<AwsS3MultipartOptions<M, B>>) {
|
||||
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<AwsS3MultipartOptions<M, B>>): 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<AwsS3STSResponse>('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<M, B>,
|
||||
{ key, uploadId, signal }: UploadResultWithSignal,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
): Promise<void> {
|
||||
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) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue