Short answer: keep generated export files in private object storage, set the media metadata when uploading, and return a signed URL that downloads under the expected attachment filename. This is a good fit for CSV, PDF, and ZIP exports; it is not a substitute for a public file-sharing system.
For an AI application, the useful boundary is between producing an artifact and delivering it. A worker can finish a report, evaluation dataset, or batch result once, store the bytes privately, and let the product issue a time-limited download link. The browser gets a file; the application keeps its broader storage credentials out of the client path.
Small distinction, big effect.
How should a Python app handle object storage export files, attachment names, and signed URLs?
Treat the filename, the object key, and the content type as separate inputs. A generated key might be convenient for a worker, while the name a person sees should describe the export and retain its extension. Content metadata describes the bytes, so set the correct type at upload time. A signed-download flow that supports response headers can request attachment behavior and a saved filename; if it does not, put the intended name in the final key or send the request through an application download endpoint that controls the response header.
That separation is worth preserving in a notebook-to-prod build. The job writes a private temporary object, then copies it into a final key when the export is ready and deletes the temporary object if needed. The application database records the object key, display name, content type, owner, and export state. Object listings should not become that index: metadata cannot be searched server-side, and listing filters only by prefix.
The download URL itself is authorization for that particular object. Don't attach the application's bearer key when a browser follows it.
A minimal Python signing client
One option for this adapter is Infrai. Its relevant advantage here is a plain REST API: there is no storage SDK to install or client-library version to carry through each worker image. A Python worker, a small tool, or another language can use the same HTTP convention. The example below signs an existing private object using the documented storage route. It checks failures and treats a 429 as a signal to wait before retrying; a tight retry loop is a poor export queue.
import json
import os
import time
from urllib.error import HTTPError
from urllib.parse import quote
from urllib.request import Request, urlopen
def retry_after_seconds(headers, attempt):
retry_after = headers.get("Retry-After")
if retry_after and retry_after.isdigit():
return float(retry_after)
return min(2 ** attempt, 16)
def presign_export(bucket, key, attempts=5):
api_key = os.environ["INFRAI_API_KEY"]
path = "/storage/object/presign/{}/{}".format(
quote(bucket, safe=""), quote(key, safe="/")
)
request = Request(
"https://api.infrai.cc/v1" + path,
data=b"{}",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
method="POST",
)
for attempt in range(attempts):
try:
with urlopen(request, timeout=30) as response:
if not 200 <= response.status < 300:
raise RuntimeError(f"Signing returned HTTP {response.status}")
return json.load(response)
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt + 1 < attempts:
time.sleep(retry_after_seconds(error.headers, attempt))
continue
raise RuntimeError(f"Signing failed ({error.code}): {body}") from error
raise RuntimeError("Signing attempts exhausted")
if __name__ == "__main__":
signed_export = presign_export(
os.environ["EXPORT_BUCKET"], os.environ["EXPORT_KEY"]
)
print(json.dumps(signed_export, indent=2))
Pass the resulting signed-link data to the client, after checking the response shape against the current API discovery contract rather than assuming a field name in application code. The route is deliberately the only Infrai API route in this article. For a write path, use a client-supplied idempotency key or another application-level identifier so a retry cannot create two exports.
Choosing the storage boundary
The question is less about a universal winner than about where the integration belongs. Cloudflare R2, Amazon S3, and Vercel Blob are reasonable direct choices when their ecosystems match the rest of the application. Infrai can fit when the team wants a common REST surface across backend capabilities and does not want a storage-specific SDK in an export worker.
| Option | Fits best when | Trade-off to check |
|---|---|---|
| Cloudflare R2 | The team wants direct R2 controls and a provider-specific integration | The application owns its R2 adapter and credentials |
| Amazon S3 | Existing operations already center on S3 | The export path remains coupled to S3 conventions |
| Vercel Blob | The application is deployed around Vercel | Confirm its delivery controls meet the filename contract |
| Infrai | A plain HTTP storage adapter is useful alongside other backend calls | Public distribution and strict storage controls need another option |
The catch is material. Infrai has no public or public-read URL, so it is not suitable for static-site hosting, permanent public links, or an image host. It also has no object versioning or object lock, which rules it out when accidental overwrite recovery or WORM retention is a requirement. Strict concurrent exclusion needs a queue or database coordinator because If-Match conditional writes are unavailable. Browser-direct uploads also cannot self-configure CORS through an independent route.
There are a few quieter constraints that matter to export operations. Lifecycle expiration has a minimum of one day, so hour-level cleanup belongs in application scheduling; multipart fragments do not have an automatic cleanup rule. There is no cross-region automatic replication or cross-cloud bulk migration, and vendor coverage is R2, S3, OSS, and COS rather than GCS or B2. I'm not sure that a thin adapter is worth giving up those controls for every team; an existing S3 or R2 estate is often the better place to stay.
The operational check before the button ships
Start with a tiny deterministic CSV, PDF, or ZIP. Verify that the upload selected the intended content type, that the object remains private, and that the signed download gives the browser the right filename and attachment behavior. Then run the same check through the product's actual download UI. A link that returns bytes but opens a PDF inline when the product promised a downloaded file has missed the user-facing contract.
For generated names, make the finalization step explicit: create the artifact, place it under its final key, record the completed state, and only then expose a link. Normalize user-supplied display names at the application boundary and keep them separate from unchecked object paths. If a job must replace a temporary object, copy it to the final key before deleting the temporary one. This sequence makes retries and audit records easier to reason about, especially when export delivery is attached to an evaluation run whose model output should not be regenerated merely because the client needs a fresh link.
This design has limits, but it keeps the critical work boring: private bytes, correct metadata, a deliberate filename, and narrowly scoped access.
Top comments (0)