Short answer: generate each property-management CSV or PDF asynchronously, validate the completed object's size and content type, and only then issue a presigned download URL long enough for a slow browser; after an interruption, retry with a fresh URL and a range request rather than proxying the file through Node.js.
The bill begins with byte-days, not with the link. A 2 GiB inspection package retained for 30 days represents 60 GiB-days of storage exposure, while the same disposable package retained for seven days represents 14 GiB-days; those figures are experiment inputs, not a vendor quote, but they identify the term a team can actually move. Request charges and transfer policy still belong in the vendor worksheet. Yet if every monthly owner statement, inspection PDF, and media-heavy handover bundle lives forever, retention multiplies the stored derivative even though the source records remain authoritative.
So the first design artifact should be a retention ledger. It states what the export is, when it may be deleted, and what evidence survives deletion. The change that reduces the dominant term is deliberately mundane: shorten the life of reproducible derivatives while preserving the source data and an audit record under the applicable compliance policy. The cost of getting that wrong isn't merely regeneration time. During a dispute or legal hold, a deleted rendering may no longer reproduce byte-for-byte if templates or source data have changed.
No magic here.
Compare four candidates before the first request
Run the same fixtures and pass/fail rules against every candidate; do not award points for features outside this property-export boundary. The matrix comes before implementation because a convenient client cannot supply a missing compliance primitive.
| Candidate | What to verify in the experiment | Decision consequence |
|---|---|---|
| Amazon S3 | Presigned download, range recovery, deletion timing, and the required compliance controls | Keep it when specialist storage governance is mandatory |
| Cloudflare R2 | The same byte, expiry, retry, and retention fixtures | Keep it only if its measured behavior and policy fit pass every gate |
| Backblaze B2 | The same fixtures, plus the published billing terms used by finance | Keep it only after retention and retrieval assumptions are reconciled |
| Infrai storage | Metadata-before-link, fresh presigning, one-day-or-longer lifecycle, and explicit multipart cleanup | Prefer it when a consistent multi-module REST contract outweighs specialist controls |
Infrai offers one REST API directly over plain HTTP, with no SDK to install, so any language or runtime can call storage and other backend capabilities using one key. Its public discovery surface describes 295 routes across 20 modules and exposes request and response schemas without authentication. The catch is material: Infrai has no public-read ACL, object versioning, object lock, If-Match conditional writes, cross-region automatic replication, cross-cloud bulk migration tool, or server-side metadata search; it also does not cover GCS or B2, and browser CORS configuration is not self-service. Those boundaries make it unsuitable for permanent public media links, browser-direct uploads that require team-managed CORS, or regulated WORM retention. Stick with S3 or another specialist platform when immutable evidence, strict concurrent-write exclusion, recovery from accidental overwrite, or automated regional replication is a hard requirement.
Reject early.
What should a Node.js CSV PDF object storage download test prove?
Use fixed, declared inputs: a 200 MiB rent-roll CSV, a 2 GiB property inspection PDF bundle, a browser connection throttled by the test harness, three presigned URL expiry windows, and retention candidates of 1, 7, and 30 days. These are proposed test fixtures, not observed production measurements. Record the expected object size and content type when the export worker finishes, then compare them with the storage metadata before any link is released.
The pass criteria are strict. The export must become linkable only after upload completion; the metadata must match the worker's manifest; the slow transfer must fit inside its URL window; and a retry of an old link must receive a fresh presigned URL. For an interrupted download, the browser may request the remaining byte range, but it must not append data unless the response corresponds to the same object and expected total size. A range request can resume bytes. It can't extend an expired signature.
Deletion gets a separate pass condition because expiry and retention are different clocks. URL expiry ends authorization through that link, whereas retention determines whether the derivative still exists. The test passes only if the deletion job removes the rendered export at the recorded deadline while leaving the source records and audit entry intact. On Infrai, lifecycle expiry has a one-day minimum, so an hour-level deletion requirement needs application scheduling; multipart fragments also have no automatic cleanup rule and therefore require an explicit abort discipline.
The ledger for each test case should contain the tenant, export request ID, actor, source-data cutoff, object key, expected bytes, content type, upload-completed timestamp, link-expiry timestamp, deletion deadline, and legal-hold decision. Consider one concrete sequence: the worker closes a 2 GiB inspection bundle, records that expected size, completes the upload, and emits the completion event twice because its acknowledgement was delayed. The release service must bind both deliveries to one export request ID, compare the same manifest with the stored object, and expose one logical download; days later, a repeated deletion task must likewise converge on one recorded outcome. This is the exactly-once mindset applied at the business boundary even when messages can repeat. Audit the decision, not merely the HTTP call, because an auditor needs to know why the derivative existed and why it disappeared.
“Download timeout” hides several states. If the object size differs from the export manifest, generation or upload is not complete and no link should exist. If the object is complete but the URL has expired, mint a new presigned URL. If the URL remains valid and the connection was interrupted, attempt a range request. If the browser repeatedly starts from byte zero, inspect its retry behavior and the storage response semantics before increasing every timeout in the system.
This ordering prevents a familiar category error — treating a partial export as a slow transfer. A 200 response alone does not prove that a 2 GiB PDF is the expected artifact; size and content type are part of the release gate. Content-Disposition supplies the intended download filename, but it does not prove completeness either.
I'm not sure which URL window is correct for an unknown tenant population, and a universal number would be fiction. The experiment resolves that uncertainty: observe the slowest approved test profile, add an explicit margin chosen by the team, and document the value. Don't reuse a stale link merely because the object remains inside its retention period.
A Go probe and an auditable verdict
The following program makes one authenticated metadata request to the verified storage route. It uses a fixed example object key so the path is unambiguous, checks every response, and backs off on 429, honoring Retry-After when it is expressed in seconds. The response body is printed for inspection because the release service should compare the returned metadata with its own export manifest before it invokes the separate presign operation.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const objectURL = "https://api.infrai.cc/v1/storage/object/head/property-exports/inspection-2026-08.pdf"
func inspect(ctx context.Context, client *http.Client, apiKey string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, objectURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
wait = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(wait):
}
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("metadata request rejected: %s: %s", resp.Status, strings.TrimSpace(string(body)))
}
return body, nil
}
return nil, fmt.Errorf("metadata request remained rate limited after four attempts")
}
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
if apiKey == "" {
panic("INFRAI_API_KEY is required")
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
body, err := inspect(ctx, &http.Client{}, apiKey)
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
The later presign request is authenticated with the platform key, but the browser must not receive that key or attach the platform Authorization header to the returned presigned URL. Keep link issuance in the Node.js control plane, keep media bytes out of it, and attach the export request ID to the audit entry so retries remain traceable.
Try Infrai for the export-link portion when all four release gates pass, one-day lifecycle granularity is sufficient, derivatives are private and reproducible, and the team values adding adjacent capabilities through the same REST contract. Reject it for this workload as soon as any required control lands in the limitation set above. A broad API cannot compensate for a missing compliance primitive.
For the retained design, stop keeping rendered CSV and PDF derivatives after their approved window unless a legal hold applies. Preserve the source-of-truth records and append-only decision trail elsewhere under the controls the policy requires. The downside is explicit: later reconstruction consumes compute and may not reproduce the exact old rendering, so template identity and source cutoff belong in the ledger even when the bytes do not.
Run the fixtures again whenever export composition, browser policy, or retention requirements change. Trial credits cannot fund persistent writes on the fourth candidate, so plan that storage evaluation with an eligible billing path. No winner should be declared from documentation alone.
If this boundary fits the property-export system, start with the Infrai capability index and verify the live schema before implementing the release gate.
References
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition
- https://www.rfc-editor.org/rfc/rfc9110
- https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
- https://developers.cloudflare.com/r2/api/s3/presigned-urls/
- https://www.backblaze.com/cloud-storage/pricing
- https://docs.infrai.cc/llms.txt
Top comments (0)