Commit 4a33d53
Chunked (resumable) uploads to get past the 100MB proxy cap (#1840)
* Add chunked (resumable) uploads to get past the 100MB proxy cap
Upstream proxies (Cloudflare) cap a single proxied request body at 100MB,
which blocked large document and zip imports through the REST endpoints.
This adds a generic chunked-upload mechanism: the client slices a file
into sub-100MB parts, uploads each independently, and the server
reassembles them before handing the whole file to the existing import
services (so there is one import code path per kind, chunked or not).
Backend:
- New models ChunkedUploadSession + ChunkedUploadPart. Parts persist
through Django storage (not local disk) so any web/worker process can
reassemble a session. Initial migration 0001_initial.
- REST endpoints: POST /api/imports/chunked/start/,
PUT|POST /api/imports/chunked/<id>/parts/<index>/,
POST /api/imports/chunked/<id>/complete/, and GET .../<id>/ for resume.
complete returns the same response shape as the matching single-request
endpoint. Part PUTs use a looser throttle scope (document_import_chunks).
- Service layer start_chunked_upload / store_chunk /
complete_chunked_upload with streaming reassembly (peak memory bounded
to one 8MB block), per-user IDOR isolation, arithmetic + integrity
validation, and fast-fail permission gating at start. All four import
kinds supported (single document + the three zip flows).
- Hourly purge_stale_chunked_uploads GC; new CHUNKED_UPLOAD_* settings.
Frontend:
- importHttp transparently routes files above CHUNK_THRESHOLD_BYTES (50MB)
through the chunked protocol; call sites and response handling unchanged.
- Raised the artificial 100MB dropzone cap (2GB single document; bulk-zip
dropzone uses the 500MB cap via new FileDropZone props).
Tests: backend round-trip/validation/IDOR/GC coverage
(test_document_imports_chunked.py) and a frontend chunked-transport suite.
CHANGELOG updated.
* Make chunked-upload temp-file cleanup non-silent
Address code-quality "empty except" findings on the best-effort temp-file
unlink in the chunked-upload assembler/completer. Extract a shared
_safe_unlink helper that swallows OSError (cleanup failure is non-fatal —
the file lingers in the OS temp dir and the original exception must not be
masked) but records it at debug level instead of an unexplained pass.
* Fix linter findings in chunked upload (pyupgrade + mypy)
- pyupgrade: drop the now-redundant quotes on the _chunk_part_path
forward-ref annotation (safe under `from __future__ import annotations`).
- mypy: restore corpus narrowing after the _resolve_corpus_for_edit
refactor by branching on `corpus is None` (the helper returns
Optional[Corpus], so the old `corpus_error is not None` check left
`corpus` typed Optional at the later `.import_content`/`.id` uses).
- mypy: widen the zip service `zip_source` params from
`UploadedFile | bytes` to `File | bytes` so the chunked completer can
hand them a plain django File (UploadedFile remains a valid subtype).
* Consolidate File import with the .base import line (consistency)
* Address review: fix chunked-upload concurrency races, public normalise_optional, COMPLETED-session retention
- store_chunk: lock the session row (select_for_update) around the check-then-create so concurrent same-index uploads can't race the uniq_chunk_part_per_session constraint into a 500.
- complete_chunked_upload: claim the session via an atomic PENDING->ASSEMBLING compare-and-swap UPDATE; a 0-row result (double-complete) is refused with 409 instead of assembling/importing twice.
- Document the DOCUMENT-kind memory behaviour: it buffers the whole file (bounded by MAX_DOCUMENT_IMPORT_SIZE_BYTES) because import_content takes bytes; the per-block streaming guarantee holds only for the ZIP kinds.
- Rename _normalise_optional -> normalise_optional (public) so views no longer import a private helper across module boundaries; update its test.
- purge_stale_chunked_uploads: also purge COMPLETED sessions older than CHUNKED_UPLOAD_COMPLETED_RETENTION_DAYS (default 30; 0 disables) so the audit-trail rows don't grow unbounded. New setting + test.
- Add clarifying comment to test_non_zip_bytes_rejected_for_zip_kind.
* Address PR #1840 review: chunked-upload resilience and GC race
Resolve the Medium and Low items from the latest review:
- Frontend uploads parts with bounded concurrency (default 4) instead of
strictly sequentially, with per-part exponential-backoff retry on
transient/5xx/network errors (4xx fail fast), and an optional onProgress
callback on every public import helper.
- purge_stale_chunked_uploads no longer purges ASSEMBLING sessions inside the
normal stale window (could delete parts under a live complete reassembly);
they are reclaimed after a longer grace window (CHUNKED_UPLOAD_ASSEMBLING_
GRACE_HOURS, default 6h) so crashed mid-assembly workers are still cleaned up.
- store_chunk reports session info from the freshly-locked row.
- ChunkedUploadCompleteView declares only [JSONParser] (no request body).
- Celery purge task exposes completed_retention_days.
- Service cap helpers use direct settings access instead of dead getattr
fallbacks; cross-reference comment between frontend chunk size and backend
part cap.
- Test imports normalise_optional from services (its real home).
- Single-document whole-file-in-RAM reassembly tracked as follow-up (#1843).
New tests: ASSEMBLING grace-window GC; frontend concurrency cap, per-part
retry, 4xx fail-fast, and progress reporting.
* Address review: chunked-upload GC batching, configurable knobs, admin, nit
- purge_stale_chunked_uploads streams each queryset via .iterator(chunk_size=100)
instead of materializing every stale session into memory (bounds peak memory
when the GC runs against a large backlog).
- Move CHUNKED_UPLOAD_ASSEMBLING_GRACE_HOURS and CHUNK_ASSEMBLY_BLOCK_SIZE to
env-backed settings in config/settings/base.py for operator tunability,
consistent with the other CHUNKED_UPLOAD_* knobs; services.py reads them from
settings (module symbols preserved for the existing test import + comments).
- Register read-only ChunkedUploadSession / ChunkedUploadPart admins so
operators can inspect stale/FAILED sessions without dropping into the shell.
- importHttp.ts: drop redundant '?? undefined' — withOptional/appendIfDefined
already guard null.
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 0de93d2 commit 4a33d53
17 files changed
Lines changed: 2755 additions & 174 deletions
File tree
- config/settings
- frontend/src
- assets/configurations
- components/widgets/modals/UploadModal
- components
- utils
- __tests__
- opencontractserver
- document_imports
- migrations
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
10 | 75 | | |
11 | 76 | | |
12 | 77 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
792 | 792 | | |
793 | 793 | | |
794 | 794 | | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
795 | 800 | | |
796 | 801 | | |
797 | 802 | | |
| |||
820 | 825 | | |
821 | 826 | | |
822 | 827 | | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
| 852 | + | |
| 853 | + | |
| 854 | + | |
| 855 | + | |
| 856 | + | |
| 857 | + | |
| 858 | + | |
| 859 | + | |
| 860 | + | |
| 861 | + | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
823 | 865 | | |
824 | 866 | | |
825 | 867 | | |
| |||
895 | 937 | | |
896 | 938 | | |
897 | 939 | | |
| 940 | + | |
| 941 | + | |
| 942 | + | |
| 943 | + | |
| 944 | + | |
898 | 945 | | |
899 | 946 | | |
900 | 947 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
137 | 137 | | |
138 | 138 | | |
139 | 139 | | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
144 | 187 | | |
145 | 188 | | |
146 | 189 | | |
147 | 190 | | |
148 | | - | |
| 191 | + | |
149 | 192 | | |
150 | | - | |
| 193 | + | |
151 | 194 | | |
152 | 195 | | |
153 | 196 | | |
| |||
Lines changed: 2 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
457 | 457 | | |
458 | 458 | | |
459 | 459 | | |
| 460 | + | |
| 461 | + | |
460 | 462 | | |
461 | 463 | | |
462 | 464 | | |
| |||
Lines changed: 16 additions & 5 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
26 | 35 | | |
27 | 36 | | |
28 | 37 | | |
| |||
39 | 48 | | |
40 | 49 | | |
41 | 50 | | |
| 51 | + | |
| 52 | + | |
42 | 53 | | |
43 | 54 | | |
44 | 55 | | |
| |||
93 | 104 | | |
94 | 105 | | |
95 | 106 | | |
96 | | - | |
| 107 | + | |
97 | 108 | | |
98 | 109 | | |
99 | 110 | | |
| |||
115 | 126 | | |
116 | 127 | | |
117 | 128 | | |
118 | | - | |
| 129 | + | |
119 | 130 | | |
120 | 131 | | |
121 | 132 | | |
122 | 133 | | |
123 | 134 | | |
124 | | - | |
| 135 | + | |
125 | 136 | | |
126 | 137 | | |
127 | 138 | | |
| |||
223 | 234 | | |
224 | 235 | | |
225 | 236 | | |
226 | | - | |
227 | | - | |
| 237 | + | |
| 238 | + | |
228 | 239 | | |
229 | 240 | | |
230 | 241 | | |
| |||
0 commit comments