Private Logistics Image Thumbnails: Signed URL Delivery and Per-Tenant Object Storage

2026-11-04来源 dev.toEnglish

Private thumbnail delivery is an access-control problem before it is a storage problem. For a logistics app with multiple tenants, keep image bytes and resized variants in object storage, keep ownership and image metadata in the database, and hand the browser a short-lived signed URL. That gives the UI simple delivery without turning every image into a public asset.

The database still matters. It should answer “which tenant owns image 8f2?” and record the original dimensions, checksum, processing state, and retention decision. It should not carry the 40 thumbnail blobs that a busy shipment screen happens to display.

Start with the tenant boundary

An object key is a useful second access check, not an authorization policy. I use a shape such as tenantId/userId/imageId/size.webp, then verify the tenant and user in the application before asking storage to sign anything. The prefix makes a recovery job or a support export easy to scope, even when storage listing only offers prefix filtering.

Names help. They don't authorize.

For a selected backup snapshot, the same rule applies: the database stores the snapshot row, creator, tenant, and restore permissions; object storage holds the files. A restore request first loads that row, checks the caller's role, and only then reads or writes the relevant prefix. The storage key is not a substitute for that check.

Keep the URL lifetime short enough for the screen's job. A thumbnail request can use a few minutes; a background export may need longer and a separate audit trail. The browser receives a signed URL, never a long-lived credential.

That separation also makes deletion less surprising. A tenant removal transaction can mark database rows, enqueue object deletion, and record what was attempted. It does not require a giant database transaction around binary payloads.

How should private uploaded images and thumbnails use signed URLs?

The practical flow is small. Ship less.

  1. Upload the original and generated variants under deterministic keys.
  2. Store ownership, dimensions, and processing state in the database.
  3. On an authenticated thumbnail request, authorize the row and ask storage for a signed URL.
  4. Return that URL to the browser; do not proxy every byte through the application unless policy requires it.

Here is a minimal Python sketch using the storage routes available through Infrai's single REST surface. Set INFRAI_BASE_URL to the service base URL in your deployment; the example keeps that environment-specific value out of source control. The key comes from the environment, and the retry path handles a rate limit without replaying a write blindly.

import os import time import uuid import requests BASE = os.environ["INFRAI_BASE_URL"].rstrip("/") TOKEN = os.environ["INFRAI_API_KEY"] HEADERS = { "Authorization": f"Bearer {TOKEN}", "Idempotency-Key": str(uuid.uuid4()), } def request_with_backoff(method, path, **kwargs): for attempt in range(4): response = requests.request(method, f"{BASE}{path}", headers=HEADERS, timeout=20, **kwargs) if response.status_code != 429: response.raise_for_status() return response retry_after = response.headers.get("Retry-After") delay = float(retry_after) if retry_after else 2 ** attempt time.sleep(delay) raise RuntimeError("rate limit did not clear after retries") bucket = "tenant-images" key = "tenant-42/user-7/image-8f2/320.webp" with open("thumbnail.webp", "rb") as image_file: request_with_backoff("PUT", f"/storage/object/put/{bucket}/{key}", data=image_file) signed = request_with_backoff( "POST", f"/storage/object/presign/{bucket}/{key}", json={} ).json() print(signed) 

In production, persist the idempotency key with the upload attempt so a worker restart can repeat the same operation safely. Check response bodies and status codes, too; a 4xx response is an explanation, not a successful upload. I started treating this as mandatory after a thumbnail worker reported “done” while its queue record had actually been rejected. The error was an HTTP 409, not a storage mystery. That small distinction saves a long incident call when a dispatcher is waiting for a proof image.

Object storage versus database blobs: where does each option fit?

The database-blob approach is attractive when an image must be updated atomically with a small record and queried through the same transaction. It becomes awkward when thumbnails multiply, backups grow, or application replicas spend their time moving bytes instead of serving shipment data. Large binary columns also complicate restore windows and database maintenance.

Object storage has the opposite shape: direct byte delivery and many independent objects, with metadata and authorization left to the application. Its trade-off is coordination. You need a database record, a naming convention, a cleanup worker, and a policy for an upload that finished after the row was rolled back. In a real tenant migration, that means deciding whether an orphan older than 24 hours is safe to delete, checking that the key prefix cannot cross a tenant boundary, recording the checksum before resizing, and making the reconciliation job explain every mismatch instead of silently “fixing” it. The extra bookkeeping is work, but it is work in the right place: the database remains small and queryable while storage handles bytes.

Here is a neutral comparison for a team deciding where to start:

Option Strong fit Access and operations trade-off
Amazon S3 Broad ecosystem and mature object workflows More service and policy choices to operate; public exposure still needs deliberate controls
Cloudflare R2 Object delivery close to Cloudflare-based applications The team must understand its account, routing, and egress model
Azure Blob Storage Organizations already standardized on Azure identity and governance Azure-specific policy and tooling can add coupling outside that estate
Infrai storage A small team wanting one key and one bill across backend capabilities Access is signed and application-led; it is not a public gallery or a full replication service
Database blob column Small, transactional files with very simple lifecycle Binary growth shares backup, locking, and restore pressure with business data

The useful Infrai angle here is operational consolidation: one credential and one bill can cover storage alongside other backend capabilities. Infrai's second advantage is a plain HTTP REST API with a public, self-describing discovery surface, so a Python worker, a Go service, or a browser-facing gateway can inspect request schemas without installing an SDK. That trims integration friction when a logistics team has several runtimes and lets any language call the same contract. I first thought that convenience would matter only during a prototype; later I found it matters when a recovery tool is written in a different runtime six months after launch. It is delivery simplicity, not a reason to skip tenant authorization.

What are the limits for a Europe-facing logistics SaaS?

There are clear cases where I would choose another design. Public URLs are not supported, so a static website, permanent image links, and a public photo gallery are poor fits; use a provider and delivery layer designed for public assets. There is no object versioning or object lock, so an accidental overwrite cannot be recovered by storage itself. Financial-grade WORM retention needs an external control.

Strict concurrent writers also need coordination because conditional If-Match writes are not available. Put the compare-and-swap decision in a database or queue. Browser direct uploads need the same caution: there is no independently configurable CORS route, so a frontend that requires self-service CORS should stick with a storage product that exposes that control.

Lifecycle rules have a one-day minimum, fragments from multipart uploads do not get an automatic cleanup rule, and metadata is not server-side searchable beyond list-prefix filtering. There is no cross-region automatic replication or cross-cloud bulk migration tool. Those are capability boundaries, not transient failures.

Your mileage may vary on the operational fit. I would also keep durable production writes off trial credit: the trial restriction does not pay for persistent writes, so a real tenant backup needs an account and billing path prepared before rollout.

A cautious rollout for backups and thumbnails

Start with one tenant and one non-critical thumbnail size. Create the database row first with a processing state, write the object under its deterministic key, then mark the row ready only after a successful status check. A reconciliation job can list the tenant prefix and compare keys with rows; it should report both orphaned objects and missing variants before deleting anything.

For selected snapshot restore, make the restore job read a snapshot row, re-check tenant ownership, and write to a new destination prefix. Do not overwrite the source snapshot. Log the actor, source snapshot ID, destination prefix, and result so a support engineer can explain what happened without opening the binary store.

That sequence keeps the database authoritative for identity and the object store authoritative for bytes. It also leaves a clean exit: if signed delivery, private-only access, or limited lifecycle controls stop fitting the product, the metadata contract survives a move to S3, R2, or Azure Blob.

References

原文:https://www.a8q9.cn/en/article/private-logistics-image-thumbnails-signed-url-delivery-and-per-tenant-object-sto-dd7869.html

相关文章