Direct-to-S3 multipart resource uploads for CKAN.
Replaces the classic "browser → CKAN app server → S3" resource-upload path
with "browser → S3" directly, using S3's native multipart upload API. CKAN
only ever hands out short-lived presigned URLs; the file's bytes never pass
through the CKAN app server. Written on top of the resource file layout and
credentials already configured for
ckanext-s3filestore —
this extension does not introduce a new storage backend or bucket
layout, it only changes how bytes get from the browser into the bucket.
ckanext-s3filestore, installed, configured, and enabled — this extension reuses itsckanext.s3filestore.*config for bucket name, region, credentials, ACL, storage path, etc. It does not work without it.- CKAN 2.10+ (developed/tested against 2.11).
- The resource form's file picker still triggers a normal OS file dialog
(native
<input type="file">, untouched — see "Compatibility" below). - Once a file is chosen, the browser:
- generates a UUID for a new resource (CKAN's
resource_createalready accepts a client-supplied id — see the "Why a client-supplied UUID is safe" section below), - calls this extension's
/multipart/startendpoint to get an S3UploadIdand key, - uploads the file in parts directly to S3 using short-lived presigned
per-part URLs from
/multipart/sign_part, - calls
/multipart/completeonce all parts have uploaded.
- generates a UUID for a new resource (CKAN's
- The form is then submitted normally (name, description, etc. included),
with
url/url_type=uploadalready populated to point at the file that is now sitting on S3 —ckanext-s3filestore's own upload code sees no file in the POST and does nothing, because there's nothing left for it to do.
CKAN core's resource schema
(ckan/logic/schema.py:default_resource_schema) validates id with
[ignore_empty, uuid_validator, resource_id_does_not_exist, unicode_safe] —
meaning resource_create already accepts and uses a caller-supplied UUID as
long as it's well-formed and not already taken. This is exactly what lets
the browser know the S3 key (which is keyed by resource id) before the
resource row exists.
This extension deliberately does not replace CKAN's tri-state
upload/link/clear widget (package/snippets/resource_upload_field.html +
resource-upload-field.js) or its own JS module. It overrides only the
basic_fields_url block of package/snippets/resource_form.html (via
{% ckan_extends %}), appending a small progress-bar/cancel widget next to
the existing native controls. When the upload finishes, it publishes CKAN
core's own resource:uploaded sandbox event, which resource-form.js
(already loaded on every resource form) uses to populate the url,
url_type, name, format, etc. fields — the same mechanism a classic
upload's post-save page state would use.
Because only that one block is touched, this composes correctly with other
extensions that override different blocks of the same template (e.g.
ckanext-spatialdata's metadata_fields block), regardless of plugin load
order.
pip install -e .Add s3_multipart_upload to ckan.plugins in your ini file, after
s3filestore:
ckan.plugins = ... s3filestore s3_multipart_uploadThe committed ckanext/s3_multipart_upload/assets/js/upload.js is a built
artifact (esbuild bundling @uppy/core + @uppy/aws-s3), checked into git
so there's no JS build step at deploy time. To rebuild after changing
frontend/src/upload.js:
cd frontend
npm install
npm run buildAll bucket/credentials config is inherited from ckanext.s3filestore.*.
This extension adds two of its own, both optional:
| Option | Default | Description |
|---|---|---|
ckanext.s3_multipart_upload.part_size_mb |
25 |
Target size per S3 part. Auto-scaled up if the file is large enough that this size would exceed S3's 10,000-part-per-upload limit. S3 requires every non-final part to be at least 5MB. |
ckanext.s3_multipart_upload.max_file_size_mb |
5000 |
Client-side upload size cap. |
None of these are things this extension can configure for you at runtime — they're one-time bucket/IAM setup, typically via Terraform or the console.
Add to whatever policy the ckanext.s3filestore.aws_access_key_id /
aws_secret_access_key (or instance role, if aws_use_ami_role) already
grants:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:CreateMultipartUpload",
"s3:UploadPart",
"s3:CompleteMultipartUpload",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts",
"s3:PutObjectTagging",
"s3:GetObjectTagging",
"s3:DeleteObjectTagging"
],
"Resource": "arn:aws:s3:::YOUR-BUCKET-NAME/*"
}
]
}The browser talks to S3 directly, so the bucket needs CORS allowing the
CKAN site's origin. ExposeHeaders: ["ETag"] is not optional — the browser
must be able to read the ETag response header for each uploaded part in
order to complete the multipart upload; S3 does not return it in the
response body.
[
{
"AllowedOrigins": ["https://your-ckan-site.example.org"],
"AllowedMethods": ["PUT"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]Two independent problems, two rules:
- Abandoned mid-upload (tab closed before all parts finished): use S3's
native
AbortIncompleteMultipartUploadlifecycle action. Needs no application code or extra IAM permissions — S3 does this itself. - Finished uploading, but the CKAN form was never saved: every object
this extension creates is tagged
ckan-status=pendingatCreateMultipartUploadtime, and only has that tag cleared once a real CKAN resource row is saved pointing at it (after_resource_create/after_resource_update). A tag-filtered expiration rule cleans up anything left in that state.
{
"Rules": [
{
"ID": "abort-incomplete-multipart-uploads",
"Status": "Enabled",
"Filter": {"Prefix": ""},
"AbortIncompleteMultipartUpload": {"DaysAfterInitiation": 1}
},
{
"ID": "expire-unconfirmed-uploads",
"Status": "Enabled",
"Filter": {"Tag": {"Key": "ckan-status", "Value": "pending"}},
"Expiration": {"Days": 3}
}
]
}sizeandmimetypeon the created resource come from an S3head_objectcall made right afterCompleteMultipartUpload(so they're authoritative, not just trusted from the browser) — but only for uploads that go through this widget.last_modifiedis set to the browser's current time at upload-completion, same semantics as (but computed independently from) the classic upload path's server-side timestamp.- No cross-page-reload upload resume: if the page is reloaded mid-upload,
the in-progress multipart upload is abandoned (cleaned up by the
AbortIncompleteMultipartUploadlifecycle rule above) and the user must pick the file again. - One file per resource, matching the existing resource form's model — this does not add multi-file/bulk upload.
pip install -e .
pip install -r dev-requirements.txt
pytest --ckan-ini=test.ini ckanext/s3_multipart_upload/testsTests use an in-process moto S3 mock (no external moto server or real AWS
account needed), following the same ckan_config/clean_db fixture
conventions as ckanext-s3filestore's own test suite. test.ini sets
ckanext.s3filestore.check_access_on_startup = false since no real bucket
exists until a per-test mock creates one.