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:
Prakash 2026-01-23 20:59:41 +05:30 committed by prakash
parent 8757a86b15
commit af1ce527f5
No known key found for this signature in database
12 changed files with 1181 additions and 200 deletions

View file

@ -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> ===

View 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>