mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
implement minimal aws-s3 plugin (#6151)
Working : - Simple uploads (putObject) - Multipart uploads - Pause / Resume / Abort , Resume works using listParts as before - Progress tracking per chunk ( `#onProgress` ) - this is temporary , until we implement xhr based progress tracking as - before. **added new examples to examples/aws-nodejs to test the rewrite , old examples will eventually be removed** Not Yet Implemented - Rate limiting – Currently uploads are sequential - Retries - Remote file uploads – Files from Google Drive, Dropbox, etc. XHR progress - full wiring with uppy state ( I saw few state bugs while testing it manually ) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Murderlon <merlijn@soverin.net>
This commit is contained in:
parent
8757a86b15
commit
af1ce527f5
12 changed files with 1181 additions and 200 deletions
|
|
@ -351,6 +351,80 @@ app.delete('/s3/multipart/:uploadId', (req, res, next) => {
|
|||
|
||||
// === </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 } = 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,
|
||||
})
|
||||
} else if (method === 'POST' && !uploadId) {
|
||||
// CreateMultipartUpload
|
||||
command = new CreateMultipartUploadCommand({
|
||||
Bucket: bucket,
|
||||
Key: key,
|
||||
})
|
||||
} 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> ===
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
|
|
@ -366,6 +440,21 @@ app.get('/withCustomEndpoints.html', (req, res) => {
|
|||
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
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
app.get('/uppy.min.mjs', (req, res) => {
|
||||
res.setHeader('Content-Type', 'text/javascript')
|
||||
|
|
@ -410,4 +499,3 @@ 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> ===
|
||||
|
|
|
|||
127
examples/aws-nodejs/public/rewrite-test.html
Normal file
127
examples/aws-nodejs/public/rewrite-test.html
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<!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,
|
||||
}),
|
||||
})
|
||||
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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue