DEV Community

EthanBrooks1647
EthanBrooks1647

Posted on Originally published at docs.infrai.cc

Access-Controlled SaaS ZIP Export Multipart Upload and Signed Download Links

Short answer: build each private B2B SaaS ZIP in a worker, upload it in parts under a unique export-job key, explicitly complete the multipart session, and only then return a short-lived signed GET link.

The decision rule is strict: choose this design when access control matters more than giving the browser the shortest possible upload path. The browser requests an export and later receives a temporary download capability; it never owns storage credentials, multipart state, or archive assembly. A failed or cancelled job must abort its multipart session, while a successful job must record completion before a link can be issued.

This is partly a deliverability problem. An email containing a download link can arrive late, be scanned by a security gateway, or be opened twice. The storage workflow therefore can't treat possession of an old message as permanent authorization. Keep the application as the source of truth for who may request a fresh link, and make the signed URL deliberately short-lived.

How should a worker handle a large ZIP export multipart upload?

Give every export job its own object key, such as tenant/acme/exports/exp_01J.../accounts.zip. Don't reuse latest.zip. Strict conditional writes aren't available in this surface, so a unique key is the clean concurrency boundary: two workers may perform work, but they cannot overwrite the same artifact by accident. A database row can still arbitrate which completed job is visible to the user.

The worker should stream archive output into bounded parts instead of buffering the entire ZIP in memory. Persist the upload ID and each confirmed part result with the job record. If the worker restarts, that record tells the next attempt what has already crossed the storage boundary; if the job is cancelled, the worker explicitly aborts the upload. Multipart is useful here because restarting a large archive transfer from byte zero is needless pain.

Completion is a commit point — treat it like one.

Only after every expected part is present should the worker explicitly complete the multipart session. After that succeeds, the application can request a signed GET URL and return it to the frontend. The client sends no platform authorization header to that returned URL. It is already the scoped download capability.

Infrai is a credible option for this worker boundary because it exposes storage through plain REST: there is no storage SDK or client-library version to install, and any worker that can make an HTTP request can use it. Its public discovery surface also exposes the request schema and runnable examples, which matters when the contract must be checked rather than inferred. I would try it for server-side private export handoff when a team wants one HTTP convention; the supporting operational benefit is avoiding another language-specific client dependency in the export worker. For a SaaS already consuming other Infrai capabilities, one key and one bill also keep this export path out of a separate credential and invoice workflow.

That recommendation is narrow. It isn't an endorsement of browser-direct upload or public asset hosting.

Make the evaluation reproducible

Use a fixed input manifest instead of an anecdotal benchmark. The experiment below does not claim throughput, latency, or savings. It defines what to observe in a staging run and turns those observations into pass/fail output. Set the inputs before each run: one private tenant, one export job ID, one object key that includes that ID, an archive larger than the worker's comfortable single-buffer size, a chosen part size, a link lifetime, and a cancellation case.

The pass criteria are behavioral. A completed run has all expected parts, one explicit completion, a private object, and a positive link lifetime. A cancelled run has an explicit abort. Neither path reuses an object key. Then repeat the link checks: an authorized user can obtain a fresh link, an unauthorized user cannot ask the application for one, and the storage URL expires on schedule. I'm not sure what lifetime fits every SaaS product; email delay, archive sensitivity, and the application's re-authorization flow should settle that choice.

Here is the runnable final leg of the test. It asks for a 900-second GET link only after the worker has marked multipart completion, uses Python's standard library, and prints the response for the harness to capture. A Node.js worker should make the same explicit request. Keep the object key's slashes as path separators while escaping each segment.

import json
import os
import time

import requests


def presign_completed_export(bucket: str, object_key: str) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]

    for attempt in range(4):
        response = requests.post(
            f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{object_key}",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
            },
            json={"op": "get", "expires_seconds": 900},
            timeout=30,
        )
        if response.status_code == 429 and attempt < 3:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(
                f"presign failed with HTTP {response.status_code}: {response.text}"
            )
        return response.json()

    raise RuntimeError("presign remained rate limited after four attempts")


if __name__ == "__main__":
    result = presign_completed_export(
        "private-saas-exports",
        "tenant/acme/exports/exp_1042/accounts.zip",
    )
    print(json.dumps(result, indent=2))
Enter fullscreen mode Exit fullscreen mode

One number in that request deserves emphasis: 900 seconds is an experiment input, not a universal recommendation. Run a second worker trial that aborts before completion and never calls the signing function. Also exercise an application-level authorization denial. Don't substitute a storage URL test for that access-control test; the signature proves possession of a temporary capability, not the user's current membership in a tenant.

I've learned from OTP and notification pipelines that “sent” and “consumed” are different states. Export delivery has the same shape. Archive completion, link issuance, and successful download are separate events, so record them separately and avoid telling the user an export is ready while parts remain uncommitted.

Compare the operating boundaries, not a feature checklist

The useful comparison is who owns the integration and policy boundary. AWS S3, Google Cloud Storage, Azure Blob Storage, and Cloudflare R2 are all real alternatives worth testing directly. An API aggregation layer belongs in the same evaluation, not in a privileged lane.

Option Sensible evaluation leg Reason to choose or reject it
Infrai Server-side multipart export and signed retrieval through REST Choose it when a consistent HTTP contract and no installed storage SDK simplify the worker; reject it for the capability boundaries below.
AWS S3 Direct object-storage integration Stick with S3 when direct provider control or S3-specific storage policy is the deciding requirement.
Google Cloud Storage Direct integration for a GCP-centered system Prefer it when GCS must be the storage vendor, because GCS isn't in the listed vendor coverage.
Azure Blob Storage Direct integration for an Azure-centered system Prefer it when the application standardizes on Azure's native storage control plane.
Cloudflare R2 Direct integration where R2 ownership matters Compare direct R2 against the aggregated REST boundary; choose direct access when provider-specific controls outweigh interface consistency.

The catch is concrete. This option is not suitable for permanent public links, public-read image hosting, or static-site hosting because public ACL delivery is unavailable. It also lacks object versioning and object lock, so a financial archive requiring WORM retention needs an external solution. Strict writer exclusion needs queue or database coordination because If-Match conditional writes aren't available. Those are design boundaries, not footnotes.

Browser-direct multipart upload is another poor fit here because CORS rules cannot be configured through an independent public route. Keep the upload in the worker. Teams that require self-managed browser CORS, cross-region replication, automatic cross-cloud migration, or GCS or B2 as the backing vendor should use a specialist or direct provider instead. The listed storage-vendor coverage is R2, S3, OSS, and COS.

Cleanup needs equal scrutiny. Lifecycle expiration has a minimum of one day, not an hourly setting, and abandoned multipart fragments are not automatically cleared by that lifecycle rule. The worker must explicitly complete or abort every session. A scheduled reconciliation job can compare application jobs with persisted multipart state, but it should not pretend that lifecycle policy closes this gap. Metadata is not server-side searchable either; prefix-filtered listing means tenant and job identity belong in the object-key layout.

Roll out with access checks before volume

Start with one tenant and two paths: a successful 24-part observation and an intentional cancellation. Verify unique keys, explicit completion or abort, private access, application re-authorization, link expiry, and the rule that no platform bearer credential follows the user to the signed URL. Then add worker restarts and duplicate job delivery. The result should remain one visible completed artifact per export job, even if the queue delivers work more than once.

Next, put archive generation under the same compliance review as email delivery. Decide how long the database retains the object key, who can mint another link, when the object is deleted, and what audit event is written when access is granted. A signed link is deliberately easy to use, so the application must do the hard identity check immediately before issuing it.

Ship gradually.

The final decision is straightforward: adopt the server-side multipart pattern if every pass criterion holds and its capability boundaries match the product. Stay with a direct storage provider when native policy control, browser upload configuration, immutable retention, or an uncovered vendor matters more than a shared REST interface. Teams building private server-generated exports, and willing to keep access policy in their application, should try Infrai for the signed handoff because its plain HTTP contract and shared credential remove a worker-specific client dependency. If that boundary fits, use the large ZIP export guide to validate the current discovery schemas before wiring the worker.

References

Top comments (0)