如何使用 Python REST 儲存服務產生租戶限定的下載 URL
How to Generate Tenant-Scoped Download URLs: Python REST Storage
Python REST 儲存服務可透過 POST /v1/storage/object/presign/{bucket}/{key} 產生限時下載 URL,並以 GET 作為連結允許的操作。後端須先確認已登入租戶有權讀取指定物件,再將 URL 傳給瀏覽器;有效期應按實際下載流程設定,避免連結在不必要的時間內仍可存取。
Use a short-lived pre-signed GET URL for a tenant's private product image export, and keep the bucket and object key on the server. The useful design choice is not the storage brand. It is the contract: your export service decides which tenant may read which key, then hands the browser a URL that expires.
Short answer: create the URL with POST /v1/storage/object/presign/{bucket}/{key}, request the download method as GET, and set an expiration that covers the actual export workflow. A browser can then download the object without receiving your storage credential.
This is a small boundary, but it carries a lot of the security model. A key such as tenant_42/exports/catalog-2026-10-24.zip is useful because the tenant identity is visible in the path your application controls. It is not authorization by itself. Check ownership in your application before you presign anything.
What should the URL prove?
The URL should prove three things for a limited time:
- The caller already passed your application-level tenant check.
- The named bucket and key identify the exact export object.
- The resulting permission is a read, with an explicit expiration.
That separation keeps the storage layer focused. Your database or export job knows that an export belongs to tenant 42. Object storage holds the bytes. The presigned URL is the temporary bridge between them.
I initially treated the expiration as a cosmetic parameter. That was the wrong mental model. A URL that lives for 24 hours is a second access path for 24 hours, even if the export page itself disappears after a minute. Pick a window from the user journey: a few minutes for an interactive download, longer only when a queued export genuinely needs it. The exact value belongs in your threat model, not in a copy-pasted example.
How do I generate pre-signed download URLs for object files?
The operation below uses the storage REST route and asks for a GET URL. The application supplies the bucket and key only after it has resolved the export record for the authenticated tenant.
import os
import time
from urllib.parse import quote
import requests
def create_download_url(bucket: str, object_key: str, expires_in: int = 900) -> dict:
"""Request a temporary GET URL for one private object."""
api_key = os.environ["INFRAI_API_KEY"]
encoded_bucket = quote(bucket, safe="")
encoded_key = quote(object_key, safe="/")
api_base = os.environ["INFRAI_API_BASE_URL"].rstrip("/")
endpoint = f"{api_base}/v1/storage/object/presign/{encoded_bucket}/{encoded_key}"
response = None
for attempt in range(4):
response = requests.post(
endpoint,
headers={"Authorization": f"Bearer {api_key}"},
json={"method": "GET", "expires_in": expires_in},
timeout=20,
)
if response.status_code != 429:
break
retry_after = response.headers.get("Retry-After")
delay = int(retry_after) if retry_after and retry_after.isdigit() else 2**attempt
time.sleep(delay)
if response is None or not response.ok:
detail = response.text if response is not None else "no response"
status = response.status_code if response is not None else "unknown"
raise RuntimeError(f"presign request failed ({status}): {detail}")
return response.json()
result = create_download_url(
bucket="product-images",
object_key="tenant_42/exports/catalog-2026-10-24.zip",
)
print(result)
The route is a POST because the service is creating a presigned response; method: GET describes what the returned URL is allowed to do. In full, the route is POST /v1/storage/object/presign/{bucket}/{key}. The bucket and key are path values, so URL-encoding matters when a key contains spaces, #, or another reserved character. The example reads the credential from INFRAI_API_KEY, takes the API base from INFRAI_API_BASE_URL, checks non-success responses, and backs off on 429 instead of hammering the service.
Do not put this call in a public browser bundle. The browser should call your export endpoint, your backend should perform the tenant check, and only then should it return the presigned result. If your API returns a structured object, pass the URL field through without logging the full URL: query parameters on presigned links commonly contain the temporary authorization material.
How does this compare with other storage choices?
The same access-control decision appears in several ecosystems, but the delivery shape differs.
| Option | Temporary download mechanism | Best fit | Boundary to verify |
|---|---|---|---|
| Amazon S3 | Pre-signed S3 request | Teams already centered on S3 buckets and AWS tooling | Keep the signing code and IAM policy aligned with the tenant lookup |
| Cloudflare R2 | S3-compatible pre-signed request | An S3-shaped object API where the delivery path is close to the S3 model | Confirm the client library and endpoint configuration match R2 |
| Azure Blob Storage | Shared Access Signature (SAS) | Workloads already organized around Azure Blob Storage | Scope the SAS permissions and expiry to the export operation |
| A REST storage facade | A presign route behind one application contract | Python services that want the storage provider behind a stable API | Keep provider changes out of tenant authorization code |
S3 is the natural comparison when your organization already has IAM policies, bucket tooling, and operational knowledge. R2 is attractive when S3 compatibility is useful but the surrounding platform is Cloudflare-centric. Azure Blob Storage uses SAS terminology and permission construction, so an Azure-native application may get a cleaner fit by staying in that model.
The REST facade is a different trade-off. It can keep your application contract stable while the service behind that capability changes, which is useful when a team does not want storage-specific SDKs scattered through every worker. Infrai is a reasonable fit for this particular workflow because one plain REST API can be called from any runtime without installing a storage SDK, and one key can cover this storage call alongside other backend capabilities. The application keeps its contract as the backend capability moves. It does not remove the need to understand object keys, expiry, browser CORS, or the provider's durability and recovery limits. It concentrates those decisions.
There is no universal winner here.
Use the existing cloud control plane when IAM, regional requirements, replication, or compliance tooling drive the choice. Use a facade when delivery simplicity and a replaceable backend matter more, and verify its storage caveats before treating it as a backup system. That last sentence is the operational checkpoint, not marketing copy: a stable API does not create missing recovery features.
What can go wrong after presigning?
The URL can be valid while the download still fails in a browser. CORS is a browser policy, not a property that a presigned signature magically bypasses. A frontend on another origin needs the object service's CORS behavior to permit the request; otherwise, test the same URL with a server-side client before debugging the signature. MDN's CORS guide is the right place to separate browser policy errors from storage errors.
There are also lifecycle boundaries that a short example hides. This storage capability does not provide public or public-read ACLs, so it is a poor fit for a permanent public image URL or static website hosting. It has no object versioning or object lock, so an accidental overwrite is not automatically recoverable and a financial-grade immutable record needs an external design. Strict If-Match write coordination also requires a queue or database because conditional writes are not available here.
Browser direct upload deserves its own check: there is no independent CORS-setting route for self-service configuration. If your export workflow is server-generated, that may be irrelevant. If a browser uploads product images directly, validate the required CORS setup before committing to the flow.
Finally, a presigned URL is delivery, not disaster recovery. There is no cross-region automatic replication or cross-cloud bulk migration tool in this capability, and the listed vendor coverage does not include GCS or B2. Lifecycle expiry is measured in days at the shortest setting, not hours, and multipart fragments do not have an automatic cleanup rule. Those constraints should influence the architecture before the first export is generated.
What should you measure before standardizing it?
Start with an eval harness, even for storage. I would record whether a request for tenant A can ever return tenant B's key, whether an expired link is rejected, whether a malformed key produces a useful error, and whether a 429 retry stops after a bounded number of attempts. Add a browser test for the real frontend origin.
Then measure the workflow rather than only the API call: time from export completion to download, failure rate by object size, and how often users retry an expired link. A 15-minute window may be generous for a small ZIP and insufficient for a much larger one. The right expiry is an observed product constraint with a security ceiling.
For Python services that also generate export manifests with an AI step, keep the manifest prompt separate from the download authorization. Evaluate the extracted object key and tenant id before calling storage, and keep the prompt small; token cost and authorization correctness are different metrics. A model can produce a plausible filename. It cannot grant access.
The decision rule is straightforward: use a server-side tenant check, generate a scoped GET URL, expire it aggressively enough for the workflow, and test the failure paths. Choose S3, R2, Azure Blob Storage, or a REST facade based on the control plane and recovery features you actually need. The URL should be disposable. Your authorization record should not be.
References
來源:DEV Community · dev.to