dash-bucket-upload 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. dash_bucket_upload-0.1.0/.babelrc +23 -0
  2. dash_bucket_upload-0.1.0/.gitignore +295 -0
  3. dash_bucket_upload-0.1.0/LICENSE +21 -0
  4. dash_bucket_upload-0.1.0/PKG-INFO +236 -0
  5. dash_bucket_upload-0.1.0/README.md +215 -0
  6. dash_bucket_upload-0.1.0/_validate_init.py +51 -0
  7. dash_bucket_upload-0.1.0/dash_bucket_upload/BucketUpload.py +140 -0
  8. dash_bucket_upload-0.1.0/dash_bucket_upload/__init__.py +71 -0
  9. dash_bucket_upload-0.1.0/dash_bucket_upload/_imports_.py +5 -0
  10. dash_bucket_upload-0.1.0/dash_bucket_upload/_keys.py +43 -0
  11. dash_bucket_upload-0.1.0/dash_bucket_upload/_s3.py +50 -0
  12. dash_bucket_upload-0.1.0/dash_bucket_upload/_validation.py +60 -0
  13. dash_bucket_upload-0.1.0/dash_bucket_upload/component.py +101 -0
  14. dash_bucket_upload-0.1.0/dash_bucket_upload/manager.py +399 -0
  15. dash_bucket_upload-0.1.0/dash_bucket_upload/metadata.json +1 -0
  16. dash_bucket_upload-0.1.0/dash_bucket_upload/package-info.json +47 -0
  17. dash_bucket_upload-0.1.0/hatch_build.py +50 -0
  18. dash_bucket_upload-0.1.0/package-lock.json +8586 -0
  19. dash_bucket_upload-0.1.0/package.json +47 -0
  20. dash_bucket_upload-0.1.0/pyproject.toml +89 -0
  21. dash_bucket_upload-0.1.0/src/lib/components/BucketUpload.react.js +245 -0
  22. dash_bucket_upload-0.1.0/src/lib/engine/__tests__/fileUpload.test.js +183 -0
  23. dash_bucket_upload-0.1.0/src/lib/engine/__tests__/partSize.test.js +44 -0
  24. dash_bucket_upload-0.1.0/src/lib/engine/__tests__/retry.test.js +73 -0
  25. dash_bucket_upload-0.1.0/src/lib/engine/__tests__/speed.test.js +36 -0
  26. dash_bucket_upload-0.1.0/src/lib/engine/__tests__/uploadEngine.test.js +118 -0
  27. dash_bucket_upload-0.1.0/src/lib/engine/api.js +47 -0
  28. dash_bucket_upload-0.1.0/src/lib/engine/fileUpload.js +137 -0
  29. dash_bucket_upload-0.1.0/src/lib/engine/partSize.js +42 -0
  30. dash_bucket_upload-0.1.0/src/lib/engine/retry.js +47 -0
  31. dash_bucket_upload-0.1.0/src/lib/engine/speed.js +43 -0
  32. dash_bucket_upload-0.1.0/src/lib/engine/transports/direct.js +75 -0
  33. dash_bucket_upload-0.1.0/src/lib/engine/transports/relay.js +26 -0
  34. dash_bucket_upload-0.1.0/src/lib/engine/uploadEngine.js +187 -0
  35. dash_bucket_upload-0.1.0/src/lib/engine/xhrPut.js +55 -0
  36. dash_bucket_upload-0.1.0/src/lib/index.js +3 -0
  37. dash_bucket_upload-0.1.0/src/lib/ui/DropZone.js +118 -0
  38. dash_bucket_upload-0.1.0/src/lib/ui/FileList.js +25 -0
  39. dash_bucket_upload-0.1.0/src/lib/ui/FileRow.js +67 -0
  40. dash_bucket_upload-0.1.0/src/lib/ui/styles.js +105 -0
  41. dash_bucket_upload-0.1.0/src/lib/utils/format.js +22 -0
  42. dash_bucket_upload-0.1.0/webpack.config.js +119 -0
@@ -0,0 +1,23 @@
1
+ {
2
+ "presets": [
3
+ "@babel/preset-env",
4
+ "@babel/preset-react"
5
+ ],
6
+ "env": {
7
+ "production": {
8
+ "plugins": [
9
+ "styled-jsx/babel"
10
+ ]
11
+ },
12
+ "development": {
13
+ "plugins": [
14
+ "styled-jsx/babel"
15
+ ]
16
+ },
17
+ "test": {
18
+ "plugins": [
19
+ "styled-jsx/babel-test"
20
+ ]
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,295 @@
1
+ # Created by .ignore support plugin (hsz.mobi)
2
+ ### Webpack
3
+ .build_cache
4
+ ### VisualStudioCode template
5
+ .vscode
6
+ .vscode/*
7
+ !.vscode/settings.json
8
+ !.vscode/tasks.json
9
+ !.vscode/launch.json
10
+ !.vscode/extensions.json
11
+ ### JetBrains template
12
+ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
13
+ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
14
+
15
+ # User-specific stuff
16
+ .idea/**/workspace.xml
17
+ .idea/**/tasks.xml
18
+ .idea/**/usage.statistics.xml
19
+ .idea/**/dictionaries
20
+ .idea/**/shelf
21
+
22
+ # Sensitive or high-churn files
23
+ .idea/**/dataSources/
24
+ .idea/**/dataSources.ids
25
+ .idea/**/dataSources.local.xml
26
+ .idea/**/sqlDataSources.xml
27
+ .idea/**/dynamic.xml
28
+ .idea/**/uiDesigner.xml
29
+ .idea/**/dbnavigator.xml
30
+
31
+ # Gradle
32
+ .idea/**/gradle.xml
33
+ .idea/**/libraries
34
+
35
+ # Gradle and Maven with auto-import
36
+ # When using Gradle or Maven with auto-import, you should exclude module files,
37
+ # since they will be recreated, and may cause churn. Uncomment if using
38
+ # auto-import.
39
+ # .idea/modules.xml
40
+ # .idea/*.iml
41
+ # .idea/modules
42
+
43
+ # CMake
44
+ cmake-build-*/
45
+
46
+ # Mongo Explorer plugin
47
+ .idea/**/mongoSettings.xml
48
+
49
+ # File-based project format
50
+ *.iws
51
+
52
+ # IntelliJ
53
+ out/
54
+
55
+ # mpeltonen/sbt-idea plugin
56
+ .idea_modules/
57
+
58
+ # JIRA plugin
59
+ atlassian-ide-plugin.xml
60
+
61
+ # Cursive Clojure plugin
62
+ .idea/replstate.xml
63
+
64
+ # Crashlytics plugin (for Android Studio and IntelliJ)
65
+ com_crashlytics_export_strings.xml
66
+ crashlytics.properties
67
+ crashlytics-build.properties
68
+ fabric.properties
69
+
70
+ # Editor-based Rest Client
71
+ .idea/httpRequests
72
+ ### Node template
73
+ # Logs
74
+ logs
75
+ *.log
76
+ npm-debug.log*
77
+ yarn-debug.log*
78
+ yarn-error.log*
79
+
80
+ # Runtime data
81
+ pids
82
+ *.pid
83
+ *.seed
84
+ *.pid.lock
85
+
86
+ # Directory for instrumented libs generated by jscoverage/JSCover
87
+ lib-cov
88
+
89
+ # Coverage directory used by tools like istanbul
90
+ coverage
91
+
92
+ # nyc test coverage
93
+ .nyc_output
94
+
95
+ # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
96
+ .grunt
97
+
98
+ # Bower dependency directory (https://bower.io/)
99
+ bower_components
100
+
101
+ # node-waf configuration
102
+ .lock-wscript
103
+
104
+ # Compiled binary addons (https://nodejs.org/api/addons.html)
105
+ build/Release
106
+
107
+ # Dependency directories
108
+ node_modules/
109
+ jspm_packages/
110
+
111
+ # TypeScript v1 declaration files
112
+ typings/
113
+
114
+ # Optional npm cache directory
115
+ .npm
116
+
117
+ # Optional eslint cache
118
+ .eslintcache
119
+
120
+ # Optional REPL history
121
+ .node_repl_history
122
+
123
+ # Output of 'npm pack'
124
+ *.tgz
125
+
126
+ # Yarn Integrity file
127
+ .yarn-integrity
128
+
129
+ # dotenv environment variables file
130
+ .env
131
+
132
+ # parcel-bundler cache (https://parceljs.org/)
133
+ .cache
134
+
135
+ # next.js build output
136
+ .next
137
+
138
+ # nuxt.js build output
139
+ .nuxt
140
+
141
+ # vuepress build output
142
+ .vuepress/dist
143
+
144
+ # Serverless directories
145
+ .serverless
146
+ ### Python template
147
+ # Byte-compiled / optimized / DLL files
148
+ __pycache__/
149
+ *.py[cod]
150
+ *$py.class
151
+
152
+ # C extensions
153
+ *.so
154
+
155
+ # Distribution / packaging
156
+ .Python
157
+ build/
158
+ develop-eggs/
159
+ dist/
160
+ downloads/
161
+ eggs/
162
+ .eggs/
163
+ lib64/
164
+ parts/
165
+ sdist/
166
+ var/
167
+ wheels/
168
+ *.egg-info/
169
+ .installed.cfg
170
+ *.egg
171
+ MANIFEST
172
+
173
+ # PyInstaller
174
+ # Usually these files are written by a python script from a template
175
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
176
+ *.manifest
177
+ *.spec
178
+
179
+ # Installer logs
180
+ pip-log.txt
181
+ pip-delete-this-directory.txt
182
+
183
+ # Unit test / coverage reports
184
+ htmlcov/
185
+ .tox/
186
+ .coverage
187
+ .coverage.*
188
+ nosetests.xml
189
+ coverage.xml
190
+ *.cover
191
+ .hypothesis/
192
+ .pytest_cache/
193
+
194
+ # Translations
195
+ *.mo
196
+ *.pot
197
+
198
+ # Django stuff:
199
+ local_settings.py
200
+ db.sqlite3
201
+
202
+ # Flask stuff:
203
+ instance/
204
+ .webassets-cache
205
+
206
+ # Scrapy stuff:
207
+ .scrapy
208
+
209
+ # Sphinx documentation
210
+ docs/_build/
211
+
212
+ # PyBuilder
213
+ target/
214
+
215
+ # Jupyter Notebook
216
+ .ipynb_checkpoints
217
+
218
+ # pyenv
219
+ .python-version
220
+
221
+ # celery beat schedule file
222
+ celerybeat-schedule
223
+
224
+ # SageMath parsed files
225
+ *.sage.py
226
+
227
+ # Environments
228
+ .venv
229
+ env/
230
+ venv/
231
+ ENV/
232
+ env.bak/
233
+ venv.bak/
234
+
235
+ # Spyder project settings
236
+ .spyderproject
237
+ .spyproject
238
+
239
+ # Rope project settings
240
+ .ropeproject
241
+
242
+ # mkdocs documentation
243
+ /site
244
+
245
+ # mypy
246
+ .mypy_cache/
247
+ ### SublimeText template
248
+ # Cache files for Sublime Text
249
+ *.tmlanguage.cache
250
+ *.tmPreferences.cache
251
+ *.stTheme.cache
252
+
253
+ # Workspace files are user-specific
254
+ *.sublime-workspace
255
+
256
+ # Project files should be checked into the repository, unless a significant
257
+ # proportion of contributors will probably not be using Sublime Text
258
+ # *.sublime-project
259
+
260
+ # SFTP configuration file
261
+ sftp-config.json
262
+
263
+ # Package control specific files
264
+ Package Control.last-run
265
+ Package Control.ca-list
266
+ Package Control.ca-bundle
267
+ Package Control.system-ca-bundle
268
+ Package Control.cache/
269
+ Package Control.ca-certs/
270
+ Package Control.merged-ca-bundle
271
+ Package Control.user-ca-bundle
272
+ oscrypto-ca-bundle.crt
273
+ bh_unicode_properties.cache
274
+
275
+ # Sublime-github package stores a github token in this file
276
+ # https://packagecontrol.io/packages/sublime-github
277
+ GitHub.sublime-settings
278
+
279
+ # Julia manifest file names
280
+ Manifest.toml
281
+ JuliaManifest.toml
282
+
283
+ # Project-specific ignores
284
+ .claude/
285
+
286
+ # Backup files
287
+ *.bak
288
+
289
+ # Built JS bundle — produced by the hatchling build hook (hatch_build.py) inside
290
+ # `uv build`, or locally via `make build`; no longer git-tracked (#40 phase 2)
291
+ dash_bucket_upload/dash_bucket_upload.min.js
292
+ dash_bucket_upload/dash_bucket_upload.min.js.map
293
+ dash_bucket_upload/dash_bucket_upload.min.js.LICENSE.txt
294
+
295
+ .ruff_cache
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Scott
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: dash-bucket-upload
3
+ Version: 0.1.0
4
+ Summary: Large-file upload component for Plotly Dash that stores files in an S3-compatible bucket
5
+ Project-URL: Homepage, https://github.com/slemke/dash-bucket-upload
6
+ Author-email: Scott Lemke <scott.r.lemke@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: dash,minio,multipart,plotly,s3,upload
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Framework :: Dash
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
17
+ Requires-Python: >=3.12
18
+ Requires-Dist: boto3>=1.34
19
+ Requires-Dist: dash<5,>=3.0.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # dash-bucket-upload
23
+
24
+ Large-file uploads for [Plotly Dash](https://dash.plotly.com/), stored in any
25
+ S3-compatible bucket (AWS S3, MinIO, Ceph, …).
26
+
27
+ A drop-in replacement for the `dcc.Upload` experience — drag-and-drop or file
28
+ picker, multiple files, per-file progress bars — that streams files into a
29
+ bucket with S3 multipart uploads instead of base64-encoding them through a
30
+ Dash callback (which adds ~33% overhead and falls over past a few hundred MB).
31
+ Handles multi-gigabyte files. Callbacks receive the bucket location and
32
+ metadata, never the file contents; end users see a normal upload widget and
33
+ nothing else.
34
+
35
+ - **Two transports, one API.** Files go either **direct** from the browser to
36
+ the bucket via presigned multipart URLs (no bytes touch the Dash server), or
37
+ are **relayed** through a streaming route on the Dash server (no bucket CORS
38
+ or browser-to-bucket connectivity needed). Your callback code is identical
39
+ in both modes.
40
+ - **Collision-free keys.** Objects are stored as
41
+ `<prefix>/<uuid4>/<original-filename>` — original names preserved, no
42
+ collisions.
43
+ - **Airgap-safe.** All JavaScript ships inside the wheel and is served locally
44
+ by Dash. No CDN, no external assets, ever.
45
+ - **Robust engine.** Parallel part uploads, exponential-backoff retries,
46
+ presigned-URL refresh on expiry, cancellation with server-side multipart
47
+ abort, throttled progress reporting.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install dash-bucket-upload # or: uv add dash-bucket-upload
53
+ ```
54
+
55
+ ## Quickstart
56
+
57
+ ```python
58
+ from dash import Dash, Input, Output, html
59
+ from dash_bucket_upload import BucketUpload, BucketUploadManager
60
+
61
+ app = Dash(__name__)
62
+
63
+ mgr = BucketUploadManager(
64
+ app,
65
+ endpoint_url = 'http://localhost:9000', # any S3-compatible store; omit for AWS S3
66
+ access_key = 'minioadmin',
67
+ secret_key = 'minioadmin',
68
+ region = 'us-east-1',
69
+ bucket = 'uploads',
70
+ prefix = 'incoming',
71
+ mode = 'direct', # or 'relay'
72
+ )
73
+
74
+ app.layout = html.Div([
75
+ BucketUpload(id = 'up', manager = mgr),
76
+ html.Div(id = 'out'),
77
+ ])
78
+
79
+ @app.callback(Output('out', 'children'), Input('up', 'lastUploadedBatch'))
80
+ def on_upload(batch):
81
+ if not batch:
82
+ return 'Nothing uploaded yet.'
83
+ # batch: [{'bucket': ..., 'key': ..., 'filename': ..., 'size': ..., 'etag': ..., 'status': 'done'}]
84
+ return f"Uploaded {len(batch)} file(s): " + ', '.join(f['key'] for f in batch)
85
+
86
+ if __name__ == '__main__':
87
+ app.run(debug = True)
88
+ ```
89
+
90
+ Try it locally with MinIO:
91
+
92
+ ```bash
93
+ docker compose -f examples/docker-compose.minio.yml up -d
94
+ uv run python examples/direct_mode.py
95
+ ```
96
+
97
+ ## S3-compatible stores
98
+
99
+ The manager speaks the S3 protocol (via boto3 as a protocol library), not
100
+ "AWS": point `endpoint_url` at any S3-compatible store and pass its
101
+ access/secret key pair using the provider-neutral parameter names
102
+ (`access_key`, `secret_key`, `session_token`, `region`,
103
+ `addressing_style`). The boto3 spellings (`aws_access_key_id`, …) are
104
+ accepted as aliases. `bucket` is a Swift *container* / GCS *bucket* /
105
+ Ceph *bucket* — same concept everywhere.
106
+
107
+ | Store | Setup notes |
108
+ |---|---|
109
+ | AWS S3 | Omit `endpoint_url`; omit credentials to use the normal AWS chain (env vars, config files, instance/IRSA roles). |
110
+ | MinIO | `endpoint_url='http(s)://minio:9000'` + root or service-account keys. Path-style addressing is applied automatically. |
111
+ | Ceph RGW | `endpoint_url` at the RGW endpoint + S3 keys from `radosgw-admin user create`. |
112
+ | 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'`. |
113
+ | 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. |
114
+
115
+ Relay mode is the lowest-friction path on stores where you can't (or don't
116
+ want to) configure CORS — the browser never talks to the store directly.
117
+
118
+ ## Choosing a transport
119
+
120
+ | | `mode='direct'` | `mode='relay'` |
121
+ |---|---|---|
122
+ | Data path | browser → bucket | browser → Dash server → bucket |
123
+ | Dash server load | none (metadata only) | streams every byte (bounded memory) |
124
+ | Browser must reach bucket | yes | no |
125
+ | Bucket CORS required | yes (see below) | no |
126
+ | Best for | biggest files, many users | locked-down networks, no CORS control |
127
+
128
+ Both modes use S3 multipart uploads under the hood and never buffer whole
129
+ files in server memory.
130
+
131
+ ## Multiple widgets, buckets, and limits
132
+
133
+ ```python
134
+ mgr = BucketUploadManager(app, ..., bucket = 'uploads') # registers 'default'
135
+ mgr.register('videos', bucket = 'media', prefix = 'video', mode = 'relay',
136
+ max_file_size = 10 * 2**30, allowed_extensions = ['.mp4', '.mkv'])
137
+
138
+ BucketUpload(id = 'up1', manager = mgr) # -> uploads/
139
+ BucketUpload(id = 'up2', manager = mgr, upload_id = 'videos') # -> media/video/
140
+ ```
141
+
142
+ Size/extension/MIME limits are enforced server-side; the component also
143
+ mirrors them client-side for instant feedback.
144
+
145
+ ## Callback-facing props
146
+
147
+ | prop | fires | contents |
148
+ |---|---|---|
149
+ | `lastUploadedBatch` | once per completed drop/selection batch | the new files: `[{bucket, key, filename, size, etag, status}]` — **use this as your callback Input** |
150
+ | `uploadedFiles` | after each file | same shape, cumulative for the component's lifetime — useful as `State` |
151
+ | `isUploading` | on change | `True` while any file is in flight |
152
+ | `progress` | throttled (~4/s) | per-file `{filename, size, loaded, percent, speedBps, status}` for custom progress UI (set `show_file_list=False`) |
153
+ | `lastError` | on failure | `{filename, message, phase}` |
154
+
155
+ ## Bucket CORS (direct mode only)
156
+
157
+ Direct mode PUTs parts from the browser straight to the bucket, so the bucket
158
+ must allow it — **including exposing the `ETag` response header**, without
159
+ which the browser cannot finish the multipart upload (the widget raises a
160
+ targeted error if this is missing).
161
+
162
+ AWS S3 (`aws s3api put-bucket-cors --bucket uploads --cors-configuration file://cors.json`):
163
+
164
+ ```json
165
+ {
166
+ "CORSRules": [{
167
+ "AllowedOrigins": ["https://your-app.example.com"],
168
+ "AllowedMethods": ["PUT", "GET", "HEAD"],
169
+ "AllowedHeaders": ["*"],
170
+ "ExposeHeaders": ["ETag"],
171
+ "MaxAgeSeconds": 3600
172
+ }]
173
+ }
174
+ ```
175
+
176
+ MinIO (`mc cors set local/uploads examples/minio-init/cors.xml`) — see
177
+ `examples/minio-init/cors.xml`; the example compose file applies it
178
+ automatically.
179
+
180
+ ## Production checklist
181
+
182
+ - **Authentication.** The upload routes are open by default. Gate them:
183
+
184
+ ```python
185
+ from flask_login import current_user
186
+ BucketUploadManager(app, ..., auth_check = lambda request: current_user.is_authenticated)
187
+ ```
188
+
189
+ - **Limits.** Set `max_file_size`, and `allowed_extensions` /
190
+ `allowed_mime_types` where applicable.
191
+ - **Abandoned uploads.** Add a bucket lifecycle rule aborting incomplete
192
+ multipart uploads (browsers closed mid-upload leave invisible parts behind):
193
+ see `examples/minio-init/lifecycle.json`; on AWS use
194
+ `AbortIncompleteMultipartUpload` with `DaysAfterInitiation: 1`.
195
+ - **Reverse proxies (relay mode).** Every part travels as one request of up to
196
+ the part size (16 MiB default, larger for huge files). Raise
197
+ `client_max_body_size` (nginx) or equivalent accordingly, and keep Flask's
198
+ `MAX_CONTENT_LENGTH` above the part size if your app sets one.
199
+ - **Credentials.** The browser never sees bucket credentials in either mode —
200
+ direct mode uses short-lived presigned URLs (`presign_expiration`, default
201
+ 1h; the engine refreshes expired URLs automatically).
202
+
203
+ ## How it works
204
+
205
+ 1. The component POSTs `create` → the server validates, generates the
206
+ collision-free key, starts an S3 multipart upload, and picks a part size
207
+ (files are split so they always fit S3's 10,000-part limit; parts are
208
+ `Blob.slice` views, so browser memory stays flat regardless of file size).
209
+ 2. Parts upload in parallel (4 per file, 6 total by default) — via presigned
210
+ URLs (direct) or the streaming `part` route (relay) — with retry and
211
+ backoff.
212
+ 3. `complete` finishes the multipart upload; the server reads the final size
213
+ from the bucket and the component fires your callback.
214
+
215
+ Cancel buttons, unmount, and failures all abort the S3 multipart upload
216
+ server-side.
217
+
218
+ ## Development
219
+
220
+ Requires Python ≥ 3.12, Node ≥ 20, [uv](https://docs.astral.sh/uv/).
221
+
222
+ ```bash
223
+ npm install # JS toolchain
224
+ npm run build # webpack bundle + regenerate the Dash component classes
225
+ uv sync # Python env (.venv)
226
+ npm test # vitest (JS engine)
227
+ uv run pytest # Python suite (moto-backed, no Docker needed)
228
+ make quality # ruff + pyright
229
+ ```
230
+
231
+ The wheel is self-building: `uv build` compiles the JS bundle via a hatchling
232
+ hook, so the bundle is never committed to git.
233
+
234
+ ## License
235
+
236
+ MIT