Uploading Objects

s3BEAR chooses one of two upload paths automatically, based on file size, so small files stay simple and large files never bottleneck on the backend.

Two upload paths

  • < 100 MB → simple upload. The file is POSTed to the backend as multipart/form-data; the backend writes it to S3.
  • ≥ 100 MB → multipart upload via presigned URLs. The file is split into 10 MB parts (configurable). The backend issues a presigned PUT URL for each part; the browser uploads the parts directly to S3-compatible storage, bypassing the backend, and the backend then finalizes the upload. This eliminates double bandwidth costs and sidesteps the FastAPI request-size limit.

The multipart flow

  1. POST /api/v1/buckets/{name}/upload/init — the backend creates the multipart upload and returns N presigned URLs.
  2. The browser uploads each part with PUT directly to S3, capturing the ETag from every response.
  3. POST /api/v1/buckets/{name}/upload/complete — the backend finalizes the object with the ordered list of ETags. (POST /upload/abort cancels an in-progress upload.)

Direct browser-to-storage transfer requires CORS on the storage layer, including an exposed ETag header. See CORS Setup.

How to use

In the UI, drag & drop into the bucket browser — the frontend routes to the correct path for you.

Programmatic simple upload:

curl -X POST "http://localhost:8200/api/v1/buckets/my-bucket/objects?prefix=2026/" \
  -H "Authorization: Bearer $TOKEN" \
  -F "[email protected]"

Configuration

Env variableDefaultPurpose
PRESIGNED_URL_BASE""External URL the browser uses to reach S3-compatible storage (URLs are signed against this host). Required when the backend and browser see storage at different hostnames.
MULTIPART_PART_SIZE_MB10Part size for multipart uploads
PRESIGNED_URL_EXPIRY_SECONDS3600Presigned URL lifetime

Why PRESIGNED_URL_BASE matters: in Kubernetes the backend reaches MinIO via http://minio.minio-ns.svc:9000 but the browser hits it via https://minio.example.com. Presigned URLs are bound to the host they were signed against — if the host doesn't match, the storage service returns 403 SignatureDoesNotMatch. See Troubleshooting.

Use case: uploading a 4 GB dataset

A data scientist uploads a 4 GB Parquet file from their browser. The frontend calls /upload/init, gets back 410 presigned URLs (4 GB ÷ 10 MB), then uploads all 410 parts to MinIO in parallel (browsers handle ~6 concurrent connections per host). The backend never touches the file payload — it only signs URLs and finalizes the manifest. The upload finishes in minutes instead of hours, and the FastAPI worker isn't tied up streaming gigabytes.


Related: CORS Setup · Configuration · Troubleshooting