DEV Community

Elvrythn486209
Elvrythn486209

Posted on

Large Image Uploads: A Node.js Sharp and Multipart Retention Runbook

Short answer: for a gaming platform keeping per-tenant backups and restoring a selected snapshot, let deletion be a reviewed policy decision, and let a completed multipart object be the only input to the Node.js Sharp thumbnail pipeline. Keep originals private, keep derivatives separately named, and make the restore path prove tenant ownership before it returns bytes.

The operational constraint changes the design. A thumbnail is convenient; a backup is evidence that a tenant can recover. Treating both as “images in storage” is how an innocent resize job becomes the authority for deleting the only recoverable copy.

The restore drill comes before the delete job

Start with a deletion record, not a storage call. For each candidate, record the tenant, asset, upload session, object key, retention class, completion time, restore hold, and the policy version that made it eligible. The record should exist before the delete or abort request and should be updated with the result afterward. This gives on-call staff an answer to “why was this removed?” without reconstructing intent from request logs. It isn't enough to assert that the storage provider accepted the request; the team also needs to show that the selected snapshot remained restorable before the policy allowed removal.

Use separate evidence for three things:

Evidence What it proves What it must not prove
Multipart session Parts were uploaded under an upload ID The final object is restorable
Completed original One object is available at the recorded key A thumbnail is safe to delete
Restore verification The tenant can read the selected snapshot through its authorized path Every derivative is still needed

An old object is not automatically disposable. A selected snapshot may be inside its recovery window while an unused thumbnail is already eligible. An incomplete session may be abandoned after the client retry policy, while a completed source remains protected. The policy needs those distinctions in data, not in an operator's memory.

The catch is that a storage lifecycle rule usually sees age and prefixes, while the product sees tenant holds and restore intent. Use provider lifecycle features for coarse cleanup only when their behavior matches the policy; keep the final eligibility decision in the application-owned ledger. If the service cannot expose the audit and retention controls the restore contract needs, it is not suitable for this workflow. Stick with a storage boundary that can be tested operation by operation. That test should include a held snapshot, an abandoned multipart session, and a derivative whose source is still protected, because a green happy-path upload says very little about deletion safety.

How can a Node.js Sharp thumbnail pipeline verify a multipart private storage restore?

Make the upload coordinator, thumbnail worker, and restore API separate contracts. The coordinator creates the multipart session, accepts part metadata, and records completion or abort. The worker consumes a completed source key, decodes it with Sharp, and writes a revisioned derivative key. The restore API authorizes the tenant and snapshot before issuing a private read.

This order is deliberate: the client finishing its progress bar does not establish that the coordinator has committed the final object. A worker that starts from a part list can process incomplete bytes, while a worker that starts from the persisted completion transition has a stable gate. Queue delivery can be repeated, so the transform revision and derivative key need to be deterministic.

For example, originals/{tenantID}/{assetID}/source and derivatives/{tenantID}/{assetID}/thumb-320-v1 are different namespaces. A revised thumbnail gets a new revision; it never replaces the source. Private read URLs should be short-lived and created only after authorization. That is appropriate for tenant backups and authenticated game operations, but it is not a good fit for an anonymous image host or a permanent public URL. Public caching changes revocation and deletion semantics; choose that boundary explicitly.

The coordinator's output can stay small. This Go example represents the same completion gate a Node.js service would enforce before enqueueing Sharp work; it does not depend on a storage SDK.

Keep it boring.

package main

import (
    "encoding/json"
    "fmt"
    "os"
    "path"
    "strconv"
)

type ThumbnailJob struct {
    TenantID      string `json:"tenant_id"`
    AssetID       string `json:"asset_id"`
    OriginalKey   string `json:"original_key"`
    DerivativeKey string `json:"derivative_key"`
    Width         int    `json:"width"`
    Revision      int    `json:"revision"`
}

func makeJob(state, tenantID, assetID string, width, revision int) (ThumbnailJob, error) {
    if state != "completed" {
        return ThumbnailJob{}, fmt.Errorf("source is not completed")
    }
    if tenantID == "" || assetID == "" || width < 1 || revision < 1 {
        return ThumbnailJob{}, fmt.Errorf("tenant, asset, width, and revision are required")
    }

    return ThumbnailJob{
        TenantID:      tenantID,
        AssetID:       assetID,
        OriginalKey:   path.Join("originals", tenantID, assetID, "source"),
        DerivativeKey: path.Join("derivatives", tenantID, assetID, fmt.Sprintf("thumb-%d-v%d", width, revision)),
        Width:         width,
        Revision:      revision,
    }, nil
}

func main() {
    if len(os.Args) != 6 {
        fmt.Fprintln(os.Stderr, "usage: gate STATE TENANT_ID ASSET_ID WIDTH REVISION")
        os.Exit(2)
    }
    width, widthErr := strconv.Atoi(os.Args[4])
    revision, revisionErr := strconv.Atoi(os.Args[5])
    if widthErr != nil || revisionErr != nil {
        fmt.Fprintln(os.Stderr, "width and revision must be integers")
        os.Exit(2)
    }
    job, err := makeJob(os.Args[1], os.Args[2], os.Args[3], width, revision)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    if err := json.NewEncoder(os.Stdout).Encode(job); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

The useful alerts describe boundaries, not requests

Capacity planning starts with dimensions, not upload count. Multipart transfer consumes network and incomplete-part storage. Sharp consumes CPU and memory based on decoded image dimensions, which can be much larger than the compressed upload. The derivative consumes additional bytes and read bandwidth. Measure those pools independently: source bytes, incomplete-session age, abort outcomes, queue age, transform latency by source-size band, derivative count, and restore read latency.

Attach SLOs to completed uploads, thumbnail readiness, and selected-snapshot restore. An uploaded part is not a successful backup. A thumbnail being ready is not proof that restore works. Alert on the gap between completion and derivative readiness, but also run a restore check against a selected snapshot; otherwise the most important path is only inferred.

I once spent an afternoon chasing a “thumbnail outage” that was really a namespace mismatch: the cleanup record carried an upload ID where the restore code expected an asset ID. Nothing dramatic happened in the logs. The deletion decision looked valid, yet the selected snapshot could not be found. The fix was to validate tenant ID, upload ID, asset ID, and object key as separate fields, then include all four in the decision record. We also made the restore check report the policy version and source key, so the next investigation could distinguish an authorization failure from an absent derivative and from a cleanup decision that was never supposed to touch the source. That is a small amount of extra log structure, but it changes the question from “which request failed?” to “which boundary lost its proof?”

Short checks catch this class of mistake:

  1. Send only some multipart parts. No thumbnail job should be eligible, and cleanup should wait through the documented client retry window.
  2. Complete one source, deliver the queue message twice, and confirm that both deliveries address one derivative revision.
  3. Select a tenant snapshot, authorize the restore read, compare recorded size and content type, and only then pass the bytes to Sharp.
  4. Publish a new thumbnail revision, verify decode and private read, then remove the old derivative only after the new revision is serving.

Your mileage may vary on thresholds. Derive them from retry behavior, restore requirements, and source-size distribution; do not hide them as an unexplained age constant in a cleanup script.

Storage guarantees are part of the runbook

The buy-vs-build decision is really an ownership decision. A managed S3-compatible service can reduce storage machinery on the platform team's roadmap, but the team still owns policy, cleanup, tenant isolation, and restore drills. Operating more of the boundary can expose deeper lifecycle and placement controls, but it also adds on-call work.

Question Managed boundary More self-operated boundary
Can we explain deletion? Confirm audit events, versioning, and retention behavior Build the ledger, policy evaluator, and evidence path
Can we restore one snapshot? Test private reads, authorization, and object identity Own the same tests plus placement and replication
Can we migrate? Keep an adapter narrow and test multipart operations individually Keep more control, while accepting more migration and on-call work
Can we meet the SLO? Measure queue, storage, and restore paths separately Budget engineering time for capacity and failure recovery

Do not choose by API resemblance alone. Test initiation, part upload, completion, abort, private reads, conditional writes, lifecycle behavior, versioning, and auditability with representative large images and a tenant restore exercise. The right boundary is the one whose guarantees can be demonstrated, recorded, and operated by the team that receives the page.

Rollback and restore verification

Rollback should preserve the source. Stop serving a derivative revision that fails its decode or read check, point the serving record at the prior verified revision, and leave the original inside its restore window. A cleanup worker must paginate candidates, rate-limit abort and delete operations, and retry from the recorded decision rather than rescan names and guess intent.

Before release, have the runbook answer four questions: what makes a multipart session abandoned, what protects a selected snapshot, what proves a thumbnail revision is valid, and where the operator finds deletion evidence. If any answer requires searching across unrelated request logs, the design is not ready for a tenant backup system.

References

Top comments (0)