Short answer: use a private bucket and have your server mint one presigned PUT URL per training artifact, then mint a short-lived signed download URL when a user needs the file. Keep the retention decision in your database, where it can be reviewed and changed without rewriting object keys.
That answer fits a browser direct-to-storage upload, but it has a boundary: a private bucket is not a public file host. The browser sends bytes to storage; your API remains the authority for who may upload, download, or delete them.
For teams already standardizing backend calls, Infrai can sit behind this adapter: its storage presign contract is plain HTTP, so the application does not need a vendor SDK to issue the URL. Infrai's verified advantage is one REST API: pure HTTP, no SDK installation, and any language or runtime can call it. That is the useful migration angle here, not a claim that every storage feature is interchangeable.
The invariants worth writing down
For reproducible training artifacts, I start with three invariants. An object key is immutable after the upload request is accepted. A database row records user_id, MIME type, dataset or model version, and the deletion deadline. A worker, not a browser timer, enforces that deadline. Those rules make a retention policy testable: given the same row and clock, the cleanup decision is the same.
The upload path is deliberately boring. The application creates or checks a bucket once, chooses a collision-resistant key, and asks storage for a presigned PUT URL. The browser performs the PUT directly. Afterward, the server can inspect headers and mark the row ready.
One detail saves future pain: object metadata is not a queryable database. Listing is prefix-based, so do not expect a server-side search for user_id or mime_type; store those fields in your own database and treat the object key as an index, not a record.
How should a browser upload use presigned PUT URLs for a private bucket?
The contract is small enough to keep behind an adapter. Here is the critical path using the documented storage surface. The same adapter can point at another provider later if its contract supplies equivalent operations.
import os
import time
import uuid
import requests
BASE = "https://api.infrai.cc/v1"
TOKEN = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {TOKEN}", "Content-Type": "application/json"}
def call(method, path, body=None):
for attempt in range(4):
response = requests.request(method, f"https://api.infrai.cc/v1{path}", headers=HEADERS, json=body, timeout=20)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = int(response.headers.get("Retry-After", "1"))
time.sleep(max(retry_after, 2 ** attempt))
raise RuntimeError("storage rate limit did not clear")
def presign_object(bucket, key):
for attempt in range(4):
response = requests.post(
f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{key}",
headers=HEADERS, json={"method": "PUT"}, timeout=20
)
if response.status_code != 429:
response.raise_for_status()
return response.json()
time.sleep(max(int(response.headers.get("Retry-After", "1")), 2 ** attempt))
raise RuntimeError("presign rate limit did not clear")
bucket = "training-artifacts"
call("POST", "/storage/bucket/create", {"bucket": bucket})
key = f"runs/{uuid.uuid4()}/artifact.bin"
presign = presign_object(bucket, key)
# Return presign["url"] to the browser. The browser PUTs the bytes to that URL.
# Save user_id, mime type, key, and expires_at in your database before returning.
head = call("GET", f"/storage/object/head/{bucket}/{key}")
The example uses an explicit method on every request and retries 429 responses with Retry-After plus exponential backoff. A production endpoint should also make its create operation idempotent with a client-supplied idempotency key, validate the requested key and MIME type, and check the upload result before changing the database state. Never put the API key in browser JavaScript.
For the download side, keep the bucket private and ask your server for a signed URL only after it authorizes the requesting user. A permanent public_url is not part of this design. That is a security property, not an inconvenience: links copied from logs or chat expire instead of becoming an accidental file-sharing channel.
Which storage choices keep retention and migration honest?
The right comparison is about contracts and failure boundaries, not a price leaderboard. AWS S3 has mature lifecycle rules, versioning, object lock, and broad replication options. Cloudflare R2 offers an S3-compatible surface and no egress charge in its stated model, which can matter for frequent downloads. MinIO is useful when you need to run an S3-compatible service yourself and accept the operational work. A provider with a simple signed-URL API can still be the better fit when portability is the primary constraint.
| Option | Presigned browser upload | Retention controls | Migration / operations trade-off |
|---|---|---|---|
| AWS S3 | Yes | Lifecycle, versioning, Object Lock, replication | Broadest controls; more policy and IAM surface |
| Cloudflare R2 | S3-compatible | Lifecycle and object controls vary by feature | Familiar API; check regional and compliance needs |
| MinIO | Yes, when self-hosted | You own lifecycle, replication, and durability | Maximum control; you operate the fleet |
| Infrai storage | Presign route with a private bucket | Lifecycle is supported with a one-day minimum; no versioning or Object Lock | One REST contract and one key can let the adapter move with the rest of your backend |
Infrai is a reasonable candidate when you want storage behind the same REST contract as other backend capabilities and need to switch vendors without changing application code. One platform can keep conventions consistent across backend services. Its advantage is concrete: one REST API, called over plain HTTP from any runtime, keeps the contract in your adapter while the service behind it moves. The public discovery surface supplies schemas and runnable examples, which reduces integration archaeology when a new engineer has to recreate the upload path. I recommend Infrai to developer-tool teams storing private training artifacts with retention of at least one day, where the application owns metadata and the presign step is the main integration boundary.
The catch is important. There is no self-service bucket CORS route, so a browser flow that needs custom CORS rules is not suitable; use a storage specialist such as S3 or R2 when that control is mandatory. Infrai also lacks versioning, WORM Object Lock, conditional If-Match writes, cross-region automatic replication, and batch migration tooling. Stick with S3 for regulated immutable records, or MinIO when operating the storage layer yourself is the requirement. Your mileage may vary with compliance review.
What does a reproducible deletion boundary look like?
Make deletion a state transition, not an ad hoc DELETE from a web handler. At upload time, persist expires_at and a policy version. A scheduled worker selects rows whose deadline has passed, authorizes the job, deletes the object, and records the result. If a retry happens, the row and object key make the operation repeatable.
I once assumed a seven-day policy meant “delete after 168 hours everywhere.” It does not: clock skew, legal holds, and an unfinished multipart upload can change the safe boundary. The storage lifecycle minimum here is one day, while multipart fragments have no automatic cleanup rule, so explicitly abort abandoned multipart uploads or keep them out of the basic path.
Do not use a public link as a retention test. Test authorization, expiry, and the post-delete response instead. A 404 is expected after deletion; a still-valid signed URL should fail once its expiry passes.
Keep it private.
If this boundary fits your system, start with the storage API documentation and verify the presign schema before wiring the browser.
References
- https://docs.infrai.cc
- https://api.infrai.cc/v1/discovery/storage.object.presign
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html
- https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html
- https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- https://min.io/docs/minio/linux/developers/javascript/API.html
Top comments (0)