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.
- dash_bucket_upload-0.1.0/.babelrc +23 -0
- dash_bucket_upload-0.1.0/.gitignore +295 -0
- dash_bucket_upload-0.1.0/LICENSE +21 -0
- dash_bucket_upload-0.1.0/PKG-INFO +236 -0
- dash_bucket_upload-0.1.0/README.md +215 -0
- dash_bucket_upload-0.1.0/_validate_init.py +51 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/BucketUpload.py +140 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/__init__.py +71 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/_imports_.py +5 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/_keys.py +43 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/_s3.py +50 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/_validation.py +60 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/component.py +101 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/manager.py +399 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/metadata.json +1 -0
- dash_bucket_upload-0.1.0/dash_bucket_upload/package-info.json +47 -0
- dash_bucket_upload-0.1.0/hatch_build.py +50 -0
- dash_bucket_upload-0.1.0/package-lock.json +8586 -0
- dash_bucket_upload-0.1.0/package.json +47 -0
- dash_bucket_upload-0.1.0/pyproject.toml +89 -0
- dash_bucket_upload-0.1.0/src/lib/components/BucketUpload.react.js +245 -0
- dash_bucket_upload-0.1.0/src/lib/engine/__tests__/fileUpload.test.js +183 -0
- dash_bucket_upload-0.1.0/src/lib/engine/__tests__/partSize.test.js +44 -0
- dash_bucket_upload-0.1.0/src/lib/engine/__tests__/retry.test.js +73 -0
- dash_bucket_upload-0.1.0/src/lib/engine/__tests__/speed.test.js +36 -0
- dash_bucket_upload-0.1.0/src/lib/engine/__tests__/uploadEngine.test.js +118 -0
- dash_bucket_upload-0.1.0/src/lib/engine/api.js +47 -0
- dash_bucket_upload-0.1.0/src/lib/engine/fileUpload.js +137 -0
- dash_bucket_upload-0.1.0/src/lib/engine/partSize.js +42 -0
- dash_bucket_upload-0.1.0/src/lib/engine/retry.js +47 -0
- dash_bucket_upload-0.1.0/src/lib/engine/speed.js +43 -0
- dash_bucket_upload-0.1.0/src/lib/engine/transports/direct.js +75 -0
- dash_bucket_upload-0.1.0/src/lib/engine/transports/relay.js +26 -0
- dash_bucket_upload-0.1.0/src/lib/engine/uploadEngine.js +187 -0
- dash_bucket_upload-0.1.0/src/lib/engine/xhrPut.js +55 -0
- dash_bucket_upload-0.1.0/src/lib/index.js +3 -0
- dash_bucket_upload-0.1.0/src/lib/ui/DropZone.js +118 -0
- dash_bucket_upload-0.1.0/src/lib/ui/FileList.js +25 -0
- dash_bucket_upload-0.1.0/src/lib/ui/FileRow.js +67 -0
- dash_bucket_upload-0.1.0/src/lib/ui/styles.js +105 -0
- dash_bucket_upload-0.1.0/src/lib/utils/format.js +22 -0
- 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
|