Large-file upload component for Plotly Dash that stores files in an S3-compatible bucket
Project description
dash-bucket-upload
Large-file uploads for Plotly Dash, stored in any S3-compatible bucket (AWS S3, MinIO, Ceph, …).
A drop-in replacement for the dcc.Upload experience — drag-and-drop or file
picker, multiple files, per-file progress bars — that streams files into a
bucket with S3 multipart uploads instead of base64-encoding them through a
Dash callback (which adds ~33% overhead and falls over past a few hundred MB).
Handles multi-gigabyte files. Callbacks receive the bucket location and
metadata, never the file contents; end users see a normal upload widget and
nothing else.
- Two transports, one API. Files go either direct from the browser to the bucket via presigned multipart URLs (no bytes touch the Dash server), or are relayed through a streaming route on the Dash server (no bucket CORS or browser-to-bucket connectivity needed). Your callback code is identical in both modes.
- Collision-free keys. Objects are stored as
<prefix>/<uuid4>/<original-filename>— original names preserved, no collisions. - Airgap-safe. All JavaScript ships inside the wheel and is served locally by Dash. No CDN, no external assets, ever.
- Robust engine. Parallel part uploads, exponential-backoff retries, presigned-URL refresh on expiry, cancellation with server-side multipart abort, throttled progress reporting.
Installation
pip install dash-bucket-upload # or: uv add dash-bucket-upload
Quickstart
from dash import Dash, Input, Output, html
from dash_bucket_upload import BucketUpload, BucketUploadManager
app = Dash(__name__)
mgr = BucketUploadManager(
app,
endpoint_url = 'http://localhost:9000', # any S3-compatible store; omit for AWS S3
access_key = 'minioadmin',
secret_key = 'minioadmin',
region = 'us-east-1',
bucket = 'uploads',
prefix = 'incoming',
mode = 'direct', # or 'relay'
)
app.layout = html.Div([
BucketUpload(id = 'up', manager = mgr),
html.Div(id = 'out'),
])
@app.callback(Output('out', 'children'), Input('up', 'lastUploadedBatch'))
def on_upload(batch):
if not batch:
return 'Nothing uploaded yet.'
# batch: [{'bucket': ..., 'key': ..., 'filename': ..., 'size': ..., 'etag': ..., 'status': 'done'}]
return f"Uploaded {len(batch)} file(s): " + ', '.join(f['key'] for f in batch)
if __name__ == '__main__':
app.run(debug = True)
Try it locally with MinIO:
docker compose -f examples/docker-compose.minio.yml up -d
uv run python examples/direct_mode.py
S3-compatible stores
The manager speaks the S3 protocol (via boto3 as a protocol library), not
"AWS": point endpoint_url at any S3-compatible store and pass its
access/secret key pair using the provider-neutral parameter names
(access_key, secret_key, session_token, region,
addressing_style). The boto3 spellings (aws_access_key_id, …) are
accepted as aliases. bucket is a Swift container / GCS bucket /
Ceph bucket — same concept everywhere.
| Store | Setup notes |
|---|---|
| AWS S3 | Omit endpoint_url; omit credentials to use the normal AWS chain (env vars, config files, instance/IRSA roles). |
| MinIO | endpoint_url='http(s)://minio:9000' + root or service-account keys. Path-style addressing is applied automatically. |
| Ceph RGW | endpoint_url at the RGW endpoint + S3 keys from radosgw-admin user create. |
| OpenStack Swift (s3api middleware) | endpoint_url at the Swift proxy's S3 endpoint; credentials are Keystone EC2-style: openstack ec2 credentials create → pass the resulting access/secret pair. region should match the Keystone region if SigV4 validation is strict (often RegionOne). Multipart uploads are backed by SLO segments and honor the same ≥5 MiB part rule. For direct mode, CORS lives on the container: swift post uploads -H 'X-Container-Meta-Access-Control-Allow-Origin: https://your-app' -H 'X-Container-Meta-Access-Control-Expose-Headers: etag'. |
| Anything else | If mc/aws s3api works against it, this does too. addressing_style='virtual' for stores behind wildcard DNS; signature_version and botocore_config are escape hatches for exotic setups. |
Relay mode is the lowest-friction path on stores where you can't (or don't want to) configure CORS — the browser never talks to the store directly.
Choosing a transport
mode='direct' |
mode='relay' |
|
|---|---|---|
| Data path | browser → bucket | browser → Dash server → bucket |
| Dash server load | none (metadata only) | streams every byte (bounded memory) |
| Browser must reach bucket | yes | no |
| Bucket CORS required | yes (see below) | no |
| Best for | biggest files, many users | locked-down networks, no CORS control |
Both modes use S3 multipart uploads under the hood and never buffer whole files in server memory.
Multiple widgets, buckets, and limits
mgr = BucketUploadManager(app, ..., bucket = 'uploads') # registers 'default'
mgr.register('videos', bucket = 'media', prefix = 'video', mode = 'relay',
max_file_size = 10 * 2**30, allowed_extensions = ['.mp4', '.mkv'])
BucketUpload(id = 'up1', manager = mgr) # -> uploads/
BucketUpload(id = 'up2', manager = mgr, upload_id = 'videos') # -> media/video/
Size/extension/MIME limits are enforced server-side; the component also mirrors them client-side for instant feedback.
Callback-facing props
| prop | fires | contents |
|---|---|---|
lastUploadedBatch |
once per completed drop/selection batch | the new files: [{bucket, key, filename, size, etag, status}] — use this as your callback Input |
uploadedFiles |
after each file | same shape, cumulative for the component's lifetime — useful as State |
isUploading |
on change | True while any file is in flight |
progress |
throttled (~4/s) | per-file {filename, size, loaded, percent, speedBps, status} for custom progress UI (set show_file_list=False) |
lastError |
on failure | {filename, message, phase} |
Bucket CORS (direct mode only)
Direct mode PUTs parts from the browser straight to the bucket, so the bucket
must allow it — including exposing the ETag response header, without
which the browser cannot finish the multipart upload (the widget raises a
targeted error if this is missing).
AWS S3 (aws s3api put-bucket-cors --bucket uploads --cors-configuration file://cors.json):
{
"CORSRules": [{
"AllowedOrigins": ["https://your-app.example.com"],
"AllowedMethods": ["PUT", "GET", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}]
}
MinIO (mc cors set local/uploads examples/minio-init/cors.xml) — see
examples/minio-init/cors.xml; the example compose file applies it
automatically.
Production checklist
-
Authentication. The upload routes are open by default. Gate them:
from flask_login import current_user BucketUploadManager(app, ..., auth_check = lambda request: current_user.is_authenticated)
-
Limits. Set
max_file_size, andallowed_extensions/allowed_mime_typeswhere applicable. -
Abandoned uploads. Add a bucket lifecycle rule aborting incomplete multipart uploads (browsers closed mid-upload leave invisible parts behind): see
examples/minio-init/lifecycle.json; on AWS useAbortIncompleteMultipartUploadwithDaysAfterInitiation: 1. -
Reverse proxies (relay mode). Every part travels as one request of up to the part size (16 MiB default, larger for huge files). Raise
client_max_body_size(nginx) or equivalent accordingly, and keep Flask'sMAX_CONTENT_LENGTHabove the part size if your app sets one. -
Credentials. The browser never sees bucket credentials in either mode — direct mode uses short-lived presigned URLs (
presign_expiration, default 1h; the engine refreshes expired URLs automatically).
How it works
- The component POSTs
create→ the server validates, generates the collision-free key, starts an S3 multipart upload, and picks a part size (files are split so they always fit S3's 10,000-part limit; parts areBlob.sliceviews, so browser memory stays flat regardless of file size). - Parts upload in parallel (4 per file, 6 total by default) — via presigned
URLs (direct) or the streaming
partroute (relay) — with retry and backoff. completefinishes the multipart upload; the server reads the final size from the bucket and the component fires your callback.
Cancel buttons, unmount, and failures all abort the S3 multipart upload server-side.
Development
Requires Python ≥ 3.12, Node ≥ 20, uv.
npm install # JS toolchain
npm run build # webpack bundle + regenerate the Dash component classes
uv sync # Python env (.venv)
npm test # vitest (JS engine)
uv run pytest # Python suite (moto-backed, no Docker needed)
make quality # ruff + pyright
The wheel is self-building: uv build compiles the JS bundle via a hatchling
hook, so the bundle is never committed to git.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dash_bucket_upload-0.1.0.tar.gz.
File metadata
- Download URL: dash_bucket_upload-0.1.0.tar.gz
- Upload date:
- Size: 104.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
938d19470df17850b3e13d80f9a9475c75b34d559b5a3264e26a3f21c023d0ec
|
|
| MD5 |
5728cef39a5231917d3d069b87f59cf3
|
|
| BLAKE2b-256 |
ac8aa1bfe4d7a086ec39cfa643e8f62740977614aabdd26e141a2cbb51e22a87
|
Provenance
The following attestation bundles were made for dash_bucket_upload-0.1.0.tar.gz:
Publisher:
publish.yml on kihaji/dash-bucket-upload
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dash_bucket_upload-0.1.0.tar.gz -
Subject digest:
938d19470df17850b3e13d80f9a9475c75b34d559b5a3264e26a3f21c023d0ec - Sigstore transparency entry: 2313907416
- Sigstore integration time:
-
Permalink:
kihaji/dash-bucket-upload@f9d8afa1e6780140d5a7ee02bf329926ccab7d2d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kihaji
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f9d8afa1e6780140d5a7ee02bf329926ccab7d2d -
Trigger Event:
push
-
Statement type:
File details
Details for the file dash_bucket_upload-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dash_bucket_upload-0.1.0-py3-none-any.whl
- Upload date:
- Size: 33.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67a4afc72665aea738b0a43fb691972ccb5f25e79ac9045e60efa872d9e16f86
|
|
| MD5 |
1e825ee06af16f16f96e84120d2b1dc9
|
|
| BLAKE2b-256 |
4afbcfb8e1ef718141b43b46528f2e1a01ffad16f3c9568d3f940100e78469e8
|
Provenance
The following attestation bundles were made for dash_bucket_upload-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on kihaji/dash-bucket-upload
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dash_bucket_upload-0.1.0-py3-none-any.whl -
Subject digest:
67a4afc72665aea738b0a43fb691972ccb5f25e79ac9045e60efa872d9e16f86 - Sigstore transparency entry: 2313907440
- Sigstore integration time:
-
Permalink:
kihaji/dash-bucket-upload@f9d8afa1e6780140d5a7ee02bf329926ccab7d2d -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kihaji
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f9d8afa1e6780140d5a7ee02bf329926ccab7d2d -
Trigger Event:
push
-
Statement type: