mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
update aws-nodejs examples (#6187)
- removed old examples , entirely remove withCustomEndpoints.html as every s3 operations are now handled by s3mini so no need of it. - add new examples and update README.md
This commit is contained in:
parent
779c19c9d7
commit
50de6d1066
7 changed files with 285 additions and 874 deletions
|
|
@ -1,7 +1,13 @@
|
|||
# Uppy + AWS S3 with Node.JS
|
||||
|
||||
A simple and fully working example of Uppy and AWS S3 storage with Node.js (and
|
||||
Express.js). It uses presigned URL at the backend level.
|
||||
A simple and fully working example of Uppy and AWS S3 storage with a Node.js
|
||||
(Express.js) backend. It demonstrates two signing modes:
|
||||
|
||||
- **Client-side signing (STS)** — The server issues temporary credentials via
|
||||
`GET /s3/sts`. The browser signs S3 requests locally using SigV4.
|
||||
- **Server-side signing (presigned URLs)** — The browser sends each S3 operation
|
||||
to `POST /s3/presign`. The server generates a presigned URL; the browser uses
|
||||
it directly.
|
||||
|
||||
## AWS Configuration
|
||||
|
||||
|
|
@ -13,8 +19,8 @@ out of the scope here.
|
|||
|
||||
### S3 Setup
|
||||
|
||||
Assuming you’re trying to setup the user `MY-UPPY-USER` to put the uploaded
|
||||
files to the bucket `MY-UPPY-BUCKET`, here’s how you can allow `MY-UPPY-USER` to
|
||||
Assuming you're trying to setup the user `MY-UPPY-USER` to put the uploaded
|
||||
files to the bucket `MY-UPPY-BUCKET`, here's how you can allow `MY-UPPY-USER` to
|
||||
get STS Federated Token and upload files to `MY-UPPY-BUCKET`:
|
||||
|
||||
1. Set CORS settings on `MY-UPPY-BUCKET` bucket:
|
||||
|
|
@ -54,8 +60,8 @@ get STS Federated Token and upload files to `MY-UPPY-BUCKET`:
|
|||
}
|
||||
```
|
||||
|
||||
3. Add the following Policy to `MY-UPPY-USER`: (if you don’t want to enable
|
||||
signing on the client, you can skip this step)
|
||||
3. Add the following Policy to `MY-UPPY-USER`: (required for client-side signing
|
||||
via the STS endpoint)
|
||||
```json
|
||||
{
|
||||
"Version": "2012-10-17",
|
||||
|
|
@ -100,9 +106,9 @@ COMPANION_AWS_SECRET=…
|
|||
PORT=8080
|
||||
```
|
||||
|
||||
N.B.: This example uses `COMPANION_AWS_` environnement variables to facilitate
|
||||
N.B.: This example uses `COMPANION_AWS_` environment variables to facilitate
|
||||
integrations with other examples in this repository, but this example does _not_
|
||||
uses Companion at all.
|
||||
use Companion at all.
|
||||
|
||||
## Enjoy it
|
||||
|
||||
|
|
|
|||
|
|
@ -1,462 +1,39 @@
|
|||
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')
|
||||
|
||||
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 {
|
||||
S3Client,
|
||||
AbortMultipartUploadCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
CreateMultipartUploadCommand,
|
||||
ListPartsCommand,
|
||||
PutObjectCommand,
|
||||
UploadPartCommand,
|
||||
} = require('@aws-sdk/client-s3')
|
||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
|
||||
const { STSClient, GetFederationTokenCommand } = require('@aws-sdk/client-sts')
|
||||
const app = express()
|
||||
const port = process.env.PORT ?? 8080
|
||||
|
||||
const policy = {
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: ['s3:PutObject'],
|
||||
Resource: [
|
||||
`arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}/*`,
|
||||
`arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}`,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
app.use(bodyParser.json())
|
||||
|
||||
/**
|
||||
* @type {S3Client}
|
||||
*/
|
||||
let s3Client
|
||||
// --- S3 signing routes ---
|
||||
app.use(require('./routes/sts'))
|
||||
app.use(require('./routes/presign'))
|
||||
|
||||
/**
|
||||
* @type {STSClient}
|
||||
*/
|
||||
let stsClient
|
||||
|
||||
const expiresIn = 900 // Define how long until a S3 signature expires.
|
||||
|
||||
function getS3Client() {
|
||||
s3Client ??= new S3Client({
|
||||
region: process.env.COMPANION_AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.COMPANION_AWS_KEY,
|
||||
secretAccessKey: process.env.COMPANION_AWS_SECRET,
|
||||
},
|
||||
forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
|
||||
})
|
||||
return s3Client
|
||||
}
|
||||
|
||||
function getSTSClient() {
|
||||
stsClient ??= new STSClient({
|
||||
region: process.env.COMPANION_AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.COMPANION_AWS_KEY,
|
||||
secretAccessKey: process.env.COMPANION_AWS_SECRET,
|
||||
},
|
||||
})
|
||||
return stsClient
|
||||
}
|
||||
|
||||
// Generate a unique S3 key for the file
|
||||
const generateS3Key = (filename) => `${crypto.randomUUID()}-${filename}`
|
||||
|
||||
// Extract the file parameters from the request
|
||||
const extractFileParameters = (req) => {
|
||||
const isPostRequest = req.method === 'POST'
|
||||
const params = isPostRequest ? req.body : req.query
|
||||
|
||||
return {
|
||||
filename: params.filename,
|
||||
contentType: params.type,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the file parameters
|
||||
const validateFileParameters = (filename, contentType) => {
|
||||
if (!filename || !contentType) {
|
||||
throw new Error(
|
||||
'Missing required parameters: filename and content type are required',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
app.use(bodyParser.urlencoded({ extended: true }), bodyParser.json())
|
||||
|
||||
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.
|
||||
|
||||
const { filename, contentType } = extractFileParameters(req)
|
||||
validateFileParameters(filename, contentType)
|
||||
|
||||
// Generate S3 key and prepare command
|
||||
const Key = generateS3Key(filename)
|
||||
|
||||
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.
|
||||
|
||||
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' })
|
||||
}
|
||||
if (typeof type !== 'string') {
|
||||
return res.status(400).json({ error: 's3: content type must be a string' })
|
||||
}
|
||||
const Key = `${crypto.randomUUID()}-${filename}`
|
||||
|
||||
const params = {
|
||||
Bucket: process.env.COMPANION_AWS_BUCKET,
|
||||
Key,
|
||||
ContentType: type,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
const command = new CreateMultipartUploadCommand(params)
|
||||
|
||||
return client.send(command, (err, data) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin)
|
||||
res.json({
|
||||
key: data.Key,
|
||||
uploadId: data.UploadId,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
function validatePartNumber(partNumber) {
|
||||
partNumber = Number(partNumber)
|
||||
return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000
|
||||
}
|
||||
app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => {
|
||||
const { uploadId, partNumber } = req.params
|
||||
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.',
|
||||
})
|
||||
}
|
||||
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 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)
|
||||
})
|
||||
|
||||
app.get('/s3/multipart/:uploadId', (req, res, next) => {
|
||||
const client = getS3Client()
|
||||
const { uploadId } = req.params
|
||||
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"',
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
listPartsPage()
|
||||
})
|
||||
|
||||
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()
|
||||
const { uploadId } = req.params
|
||||
const { key } = req.query
|
||||
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"',
|
||||
})
|
||||
}
|
||||
if (!Array.isArray(parts) || !parts.every(isValidPart)) {
|
||||
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,
|
||||
},
|
||||
}),
|
||||
(err, data) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin)
|
||||
res.json({
|
||||
location: data.Location,
|
||||
})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
app.delete('/s3/multipart/:uploadId', (req, res, next) => {
|
||||
const client = getS3Client()
|
||||
const { uploadId } = req.params
|
||||
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 client.send(
|
||||
new AbortMultipartUploadCommand({
|
||||
Bucket: process.env.COMPANION_AWS_BUCKET,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
}),
|
||||
(err) => {
|
||||
if (err) {
|
||||
next(err)
|
||||
return
|
||||
}
|
||||
res.json({})
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// === </S3 MULTIPART> ===
|
||||
|
||||
// === <S3 Pre-signed URL Endpoint for Plugin Rewrite v3> ===
|
||||
// This endpoint returns pre-signed URLs for S3 operations
|
||||
// Used by the rewritten @uppy/aws-s3 plugin with signRequest option
|
||||
|
||||
app.post('/s3/presign', async (req, res, next) => {
|
||||
try {
|
||||
const { method, key, uploadId, partNumber, contentType } = req.body
|
||||
const client = getS3Client()
|
||||
|
||||
if (!method || !key) {
|
||||
return res.status(400).json({ error: 'method and key are required' })
|
||||
}
|
||||
|
||||
const bucket = process.env.COMPANION_AWS_BUCKET
|
||||
let command
|
||||
|
||||
// Determine which command to use based on method and params
|
||||
if (method === 'PUT' && uploadId && partNumber) {
|
||||
// UploadPart
|
||||
command = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: parseInt(partNumber, 10),
|
||||
})
|
||||
} else if (method === 'PUT') {
|
||||
// PutObject (simple upload)
|
||||
command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
})
|
||||
} else if (method === 'POST' && !uploadId) {
|
||||
// CreateMultipartUpload
|
||||
command = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
})
|
||||
} else if (method === 'POST' && uploadId) {
|
||||
// CompleteMultipartUpload
|
||||
command = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
// Note: parts are sent in the request body, not in the presigned URL
|
||||
})
|
||||
} else if (method === 'DELETE' && uploadId) {
|
||||
// AbortMultipartUpload
|
||||
command = new AbortMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
})
|
||||
} else if (method === 'GET' && uploadId) {
|
||||
// ListParts
|
||||
command = new ListPartsCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
})
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Unsupported operation' })
|
||||
}
|
||||
|
||||
const url = await getSignedUrl(client, command, { expiresIn: 900 })
|
||||
|
||||
res.setHeader('Access-Control-Allow-Origin', accessControlAllowOrigin)
|
||||
res.json({ url })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
// === </S3 Pre-signed URL Endpoint for Plugin Rewrite v3> ===
|
||||
|
||||
// === <some plumbing to make the example work> ===
|
||||
// ---------------------------------------------------------------------------
|
||||
// Static file serving
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
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('/rewrite-test.html', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
// Inject bucket config as JS variables
|
||||
require('node:fs').readFile(htmlPath, 'utf8', (err, html) => {
|
||||
if (err) return res.status(500).send('Error loading page')
|
||||
// Inject bucket/region config so the client can read them.
|
||||
const config = `<script>
|
||||
window.UPPY_S3_BUCKET = "${process.env.COMPANION_AWS_BUCKET}";
|
||||
window.UPPY_S3_REGION = "${process.env.COMPANION_AWS_REGION}";
|
||||
</script>`
|
||||
const htmlPath = path.join(__dirname, 'public', 'rewrite-test.html')
|
||||
require('node:fs').readFile(htmlPath, 'utf8', (err, html) => {
|
||||
if (err) return res.status(500).send('Error loading page')
|
||||
// Inject config before </head>
|
||||
const modifiedHtml = html.replace('</head>', `${config}</head>`)
|
||||
res.send(modifiedHtml)
|
||||
res.setHeader('Content-Type', 'text/html')
|
||||
res.send(html.replace('</head>', `${config}</head>`))
|
||||
})
|
||||
})
|
||||
app.get('/index.html', (req, res) => {
|
||||
res.setHeader('Location', '/').sendStatus(308).end()
|
||||
})
|
||||
|
||||
app.get('/uppy.min.mjs', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
|
|
|
|||
|
|
@ -2,69 +2,117 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS upload example</title>
|
||||
<title>Uppy – AWS S3 upload example</title>
|
||||
<link href="./uppy.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
h1 { color: #1269cf; }
|
||||
details { margin: 20px 0; }
|
||||
summary { cursor: pointer; font-weight: bold; padding: 10px; background: #f5f5f5; border-radius: 4px; }
|
||||
.description { margin: 10px 0; color: #555; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AWS upload example</h1>
|
||||
<h1>AWS S3 upload example</h1>
|
||||
<p>Demonstrates the <code>@uppy/aws-s3</code> plugin with two signing modes.</p>
|
||||
|
||||
<details open name="uppy">
|
||||
<summary>Sign on the server</summary>
|
||||
<div id="uppy-sign-on-server"></div>
|
||||
<summary>Sign on the client (STS credentials)</summary>
|
||||
<p class="description">
|
||||
Uses <code>getCredentials</code> to fetch temporary STS credentials from
|
||||
<code>/s3/sts</code>. The browser signs all S3 requests locally using SigV4.
|
||||
</p>
|
||||
<div id="uppy-sts"></div>
|
||||
</details>
|
||||
|
||||
<details name="uppy">
|
||||
<summary>Sign on the client (if WebCrypto is available)</summary>
|
||||
<div id="uppy-sign-on-client"></div>
|
||||
<summary>Sign on the server (presigned URLs)</summary>
|
||||
<p class="description">
|
||||
Uses <code>signRequest</code> to call <code>/s3/presign</code> for each S3
|
||||
operation. The server generates presigned URLs; the browser uses them directly.
|
||||
</p>
|
||||
<div id="uppy-sign"></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 './uppy.min.mjs'
|
||||
import { Uppy, Dashboard, AwsS3, GoldenRetriever } from './uppy.min.mjs'
|
||||
|
||||
function onUploadComplete(result) {
|
||||
console.log(
|
||||
'Upload complete! We’ve uploaded these files:',
|
||||
result.successful,
|
||||
)
|
||||
console.log('Upload complete!', result.successful)
|
||||
}
|
||||
function onUploadSuccess(file, data) {
|
||||
console.log(
|
||||
'Upload success! We’ve uploaded this file:',
|
||||
file.meta['name'],
|
||||
)
|
||||
console.log('Upload success:', file.meta.name, data)
|
||||
}
|
||||
function onUploadError(file, error) {
|
||||
console.error('Upload error:', file?.meta?.name, error)
|
||||
}
|
||||
|
||||
// Mode 1: Client-side signing with STS credentials
|
||||
{
|
||||
const uppy = new Uppy()
|
||||
const uppy = new Uppy({ debug: true })
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy-sign-on-server',
|
||||
target: '#uppy-sts',
|
||||
height: 400,
|
||||
})
|
||||
.use(AwsS3, {
|
||||
id: 'myAWSPlugin',
|
||||
endpoint: '/',
|
||||
bucket: window.UPPY_S3_BUCKET,
|
||||
region: window.UPPY_S3_REGION,
|
||||
getCredentials: async ({ signal }) => {
|
||||
const response = await fetch('/s3/sts', { signal })
|
||||
if (!response.ok) throw new Error('Failed to get credentials')
|
||||
const data = await response.json()
|
||||
return {
|
||||
credentials: {
|
||||
accessKeyId: data.credentials.AccessKeyId,
|
||||
secretAccessKey: data.credentials.SecretAccessKey,
|
||||
sessionToken: data.credentials.SessionToken,
|
||||
expiration: data.credentials.Expiration,
|
||||
},
|
||||
region: data.region,
|
||||
}
|
||||
},
|
||||
})
|
||||
.use(GoldenRetriever)
|
||||
|
||||
uppy.on('complete', onUploadComplete)
|
||||
uppy.on('upload-success', onUploadSuccess)
|
||||
uppy.on('upload-error', onUploadError)
|
||||
}
|
||||
|
||||
// Mode 2: Server-side signing with presigned URLs
|
||||
{
|
||||
const uppy = new Uppy()
|
||||
const uppy = new Uppy({ debug: true })
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy-sign-on-client',
|
||||
target: '#uppy-sign',
|
||||
height: 400,
|
||||
})
|
||||
.use(AwsS3, {
|
||||
id: 'myAWSPlugin',
|
||||
endpoint: '/',
|
||||
getTemporarySecurityCredentials: typeof crypto?.subtle === 'object',
|
||||
bucket: window.UPPY_S3_BUCKET,
|
||||
region: window.UPPY_S3_REGION,
|
||||
signRequest: async (request) => {
|
||||
const response = await fetch('/s3/presign', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
method: request.method,
|
||||
key: request.key,
|
||||
uploadId: request.uploadId,
|
||||
partNumber: request.partNumber,
|
||||
contentType: request.contentType,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to get presigned URL')
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
.use(GoldenRetriever)
|
||||
|
||||
uppy.on('complete', onUploadComplete)
|
||||
uppy.on('upload-success', onUploadSuccess)
|
||||
uppy.on('upload-error', onUploadError)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -1,126 +0,0 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS S3 Plugin Rewrite Test</title>
|
||||
<link href="./uppy.min.css" rel="stylesheet" />
|
||||
<style>
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
h1 { color: #1269cf; }
|
||||
.test-section { margin: 20px 0; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; }
|
||||
.test-section h2 { margin-top: 0; color: #333; }
|
||||
footer { margin-top: 40px; color: #666; font-size: 14px; }
|
||||
details { margin: 20px 0; }
|
||||
summary { cursor: pointer; font-weight: bold; padding: 10px; background: #f5f5f5; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AWS S3 Plugin Rewrite Test</h1>
|
||||
<p>Testing the rewritten <code>@uppy/aws-s3</code> plugin using S3mini.</p>
|
||||
|
||||
<details open name="test">
|
||||
<summary>Test 1: Using getCredentials (STS)</summary>
|
||||
<div class="test-section">
|
||||
<p>Uses the <code>/s3/sts</code> endpoint to get temporary credentials. S3mini handles signing internally.</p>
|
||||
<div id="uppy-sts"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details name="test">
|
||||
<summary>Test 2: Using signRequest (Server Signing)</summary>
|
||||
<div class="test-section">
|
||||
<p>Uses the <code>/s3/presign</code> endpoint to sign each request on the server.</p>
|
||||
<div id="uppy-sign"></div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<footer>
|
||||
<a href="./index.html">← Back to original example</a>
|
||||
</footer>
|
||||
|
||||
<noscript>You need JavaScript to run this example.</noscript>
|
||||
<script type="module">
|
||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
||||
|
||||
// Common event handlers
|
||||
function onUploadComplete(result) {
|
||||
console.log('Upload complete!', result.successful)
|
||||
}
|
||||
function onUploadSuccess(file, data) {
|
||||
console.log('Upload success:', file.meta.name, data)
|
||||
}
|
||||
function onUploadError(file, error) {
|
||||
console.error('Upload error:', file?.meta?.name, error)
|
||||
}
|
||||
|
||||
// Test 1: Using getCredentials (STS)
|
||||
{
|
||||
const uppy = new Uppy({ debug: true })
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy-sts',
|
||||
height: 400,
|
||||
})
|
||||
.use(AwsS3, {
|
||||
id: 'AwsS3-STS',
|
||||
bucket: window.UPPY_S3_BUCKET, // Set by server
|
||||
region: window.UPPY_S3_REGION || 'us-east-1',
|
||||
// Get temporary credentials from /s3/sts
|
||||
getCredentials: async ({ signal }) => {
|
||||
const response = await fetch('/s3/sts', { signal })
|
||||
if (!response.ok) throw new Error('Failed to get credentials')
|
||||
const data = await response.json()
|
||||
return {
|
||||
credentials: {
|
||||
accessKeyId: data.credentials.AccessKeyId,
|
||||
secretAccessKey: data.credentials.SecretAccessKey,
|
||||
sessionToken: data.credentials.SessionToken,
|
||||
},
|
||||
bucket: data.bucket,
|
||||
region: data.region,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
uppy.on('complete', onUploadComplete)
|
||||
uppy.on('upload-success', onUploadSuccess)
|
||||
uppy.on('upload-error', onUploadError)
|
||||
}
|
||||
|
||||
// Test 2: Using signRequest (Server Signing)
|
||||
{
|
||||
const uppy = new Uppy({ debug: true })
|
||||
.use(Dashboard, {
|
||||
inline: true,
|
||||
target: '#uppy-sign',
|
||||
height: 400,
|
||||
})
|
||||
.use(AwsS3, {
|
||||
id: 'AwsS3-Sign',
|
||||
bucket: window.UPPY_S3_BUCKET,
|
||||
region: window.UPPY_S3_REGION || 'us-east-1',
|
||||
// Sign requests via /s3/presign endpoint (returns presigned URLs)
|
||||
signRequest: async (request) => {
|
||||
const response = await fetch('/s3/presign', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
method: request.method,
|
||||
key: request.key,
|
||||
uploadId: request.uploadId,
|
||||
partNumber: request.partNumber,
|
||||
contentType: request.contentType,
|
||||
}),
|
||||
})
|
||||
if (!response.ok) throw new Error('Failed to get presigned URL')
|
||||
return response.json()
|
||||
},
|
||||
})
|
||||
|
||||
uppy.on('complete', onUploadComplete)
|
||||
uppy.on('upload-success', onUploadSuccess)
|
||||
uppy.on('upload-error', onUploadError)
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
<!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>
|
||||
108
examples/aws-nodejs/routes/presign.js
Normal file
108
examples/aws-nodejs/routes/presign.js
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* POST /s3/presign — Presigned URLs for server-side signing (signRequest)
|
||||
*
|
||||
* The client sends the S3 operation details (method, key, uploadId, etc.)
|
||||
* and this endpoint returns a presigned URL. The browser then sends the
|
||||
* actual request directly to S3 using that URL.
|
||||
*/
|
||||
|
||||
const { Router } = require('express')
|
||||
const {
|
||||
S3Client,
|
||||
AbortMultipartUploadCommand,
|
||||
CompleteMultipartUploadCommand,
|
||||
CreateMultipartUploadCommand,
|
||||
ListPartsCommand,
|
||||
PutObjectCommand,
|
||||
UploadPartCommand,
|
||||
} = require('@aws-sdk/client-s3')
|
||||
const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
|
||||
|
||||
const expiresIn = 900 // 15 minutes
|
||||
|
||||
let s3Client
|
||||
function getS3Client() {
|
||||
s3Client ??= new S3Client({
|
||||
region: process.env.COMPANION_AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.COMPANION_AWS_KEY,
|
||||
secretAccessKey: process.env.COMPANION_AWS_SECRET,
|
||||
},
|
||||
forcePathStyle: process.env.COMPANION_AWS_FORCE_PATH_STYLE === 'true',
|
||||
})
|
||||
return s3Client
|
||||
}
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.post('/s3/presign', async (req, res, next) => {
|
||||
// Before giving the presigned URL to the client, you should first check if
|
||||
// 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.
|
||||
|
||||
try {
|
||||
const { method, key, uploadId, partNumber, contentType } = req.body
|
||||
const client = getS3Client()
|
||||
const bucket = process.env.COMPANION_AWS_BUCKET
|
||||
|
||||
if (!method || !key) {
|
||||
return res.status(400).json({ error: 'method and key are required' })
|
||||
}
|
||||
|
||||
let command
|
||||
|
||||
if (method === 'PUT' && uploadId && partNumber) {
|
||||
// UploadPart (multipart)
|
||||
command = new UploadPartCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
PartNumber: parseInt(partNumber, 10),
|
||||
})
|
||||
} else if (method === 'PUT' && !uploadId && !partNumber) {
|
||||
// PutObject (simple upload)
|
||||
command = new PutObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
})
|
||||
} else if (method === 'POST' && !uploadId) {
|
||||
// CreateMultipartUpload
|
||||
command = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
ContentType: contentType || 'application/octet-stream',
|
||||
})
|
||||
} else if (method === 'POST' && uploadId) {
|
||||
// CompleteMultipartUpload
|
||||
command = new CompleteMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
})
|
||||
} else if (method === 'DELETE' && uploadId) {
|
||||
// AbortMultipartUpload
|
||||
command = new AbortMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
})
|
||||
} else if (method === 'GET' && uploadId) {
|
||||
// ListParts
|
||||
command = new ListPartsCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
UploadId: uploadId,
|
||||
})
|
||||
} else {
|
||||
return res.status(400).json({ error: 'Unsupported operation' })
|
||||
}
|
||||
|
||||
const url = await getSignedUrl(client, command, { expiresIn })
|
||||
res.json({ url })
|
||||
} catch (err) {
|
||||
next(err)
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
65
examples/aws-nodejs/routes/sts.js
Normal file
65
examples/aws-nodejs/routes/sts.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* GET /s3/sts — Temporary credentials for client-side signing (getCredentials)
|
||||
*
|
||||
* Returns short-lived STS credentials so the browser can sign S3 requests
|
||||
* locally using SigV4. The credentials are scoped to PutObject only.
|
||||
*/
|
||||
|
||||
const { Router } = require('express')
|
||||
const { STSClient, GetFederationTokenCommand } = require('@aws-sdk/client-sts')
|
||||
|
||||
const expiresIn = 900 // 15 minutes
|
||||
|
||||
// IAM policy for the federated user — allows PutObject to the bucket.
|
||||
const policy = {
|
||||
Version: '2012-10-17',
|
||||
Statement: [
|
||||
{
|
||||
Effect: 'Allow',
|
||||
Action: ['s3:PutObject'],
|
||||
Resource: [
|
||||
`arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}/*`,
|
||||
`arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}`,
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
let stsClient
|
||||
function getSTSClient() {
|
||||
stsClient ??= new STSClient({
|
||||
region: process.env.COMPANION_AWS_REGION,
|
||||
credentials: {
|
||||
accessKeyId: process.env.COMPANION_AWS_KEY,
|
||||
secretAccessKey: process.env.COMPANION_AWS_SECRET,
|
||||
},
|
||||
})
|
||||
return stsClient
|
||||
}
|
||||
|
||||
const router = Router()
|
||||
|
||||
router.get('/s3/sts', (req, res, next) => {
|
||||
// Before giving the STS token to the client, you should first check if 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',
|
||||
DurationSeconds: expiresIn,
|
||||
Policy: JSON.stringify(policy),
|
||||
}),
|
||||
)
|
||||
.then((response) => {
|
||||
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)
|
||||
})
|
||||
|
||||
module.exports = router
|
||||
Loading…
Add table
Add a link
Reference in a new issue