AnnouncementsIntroducing Arkoren

    Menu

    Uploads and Downloads

    Store bounded uploads under safe keys and serve authorized objects with attachment, inline, and range behavior.

    What's on this page
    1. Bound multipart requests and validate individual files before storage.
    2. Generate upload keys or deliberately replace an authorized application key.
    3. Serve authorized attachments and restricted inline content with backend-specific range behavior.
    4. Preserve transfer and cleanup errors without replacing a committed response.
    On this page

    An upload brings untrusted bytes into your store; a download exposes those bytes to another client. Storage’s HTTP helpers generate upload keys and control how objects are presented, but your handler still decides who may write or read each object and which content your application accepts.

    Examples assume a configured files *storage.Storage, r *http.Request, and w http.ResponseWriter in an error-returning router handler. Import request from arkoren.dev/core/http/request and validate from arkoren.dev/foundation/validate. Backend construction belongs on Drivers.

    Accepting Uploads

    For an avatar upload, authorize the current user’s profile before storing a file. Parse the whole multipart request with a limit that includes fields and multipart overhead, not only file bytes:

    form, err := request.Multipart(r, 5<<20)
    if err != nil {
    	return err
    }

    This accepts at most 5 MiB in the whole body; exceeding the limit returns a 413 problem. Parse Uploads owns general multipart parsing, repeated-call behavior, and temporary-file cleanup. Through Arkoren’s router, cleanup runs after the response. Outside that adapter, the caller must arrange form.RemoveAll; consume and close file handles before cleanup, and do not queue file headers for later use.

    Set a route ceiling with Body Limits as well. A negative request.Multipart limit uses the contextual middleware limit, or the default when none is set; it does not mean unlimited. The parser keeps up to 8 MiB of file content in memory before spilling larger parts to disk.

    The whole-body limit does not express an avatar policy. Define a separate policy requiring a PNG of at most 2 MiB:

    avatar := validate.File(func(upload *validate.FileBuilder) {
    	upload.Required().Max(2 << 20).Types("image/png")
    })

    Validate the actual *multipart.FileHeader before storage. A missing field returns nil from form.File; Required rejects it before the storage helper can dereference it:

    header := form.File("avatar")
    if err := avatar.Validate(r.Context(), header); err != nil {
    	return request.ValidationProblem(err)
    }
    info, err := files.PutFile(r.Context(), "avatars", header)

    On success, persist info.Key with the owner and any display metadata. The validation adapter maps rule violations to an HTTP problem while preserving operational errors; see Validation Errors. File Upload Rules explain per-file size, claimed extension, and detected-type checks. Detection examines leading bytes, not the client’s Content-Type header; it is not malware scanning or proof that an image decoder will accept the whole file.

    Choosing Keys

    PutFile combines your directory with 26 random lowercase base32 characters and an extension inferred from the first 512 bytes. The client filename never becomes the key. Collisions are extremely unlikely, not impossible: the underlying Put still replaces a colliding key.

    The directory may be empty; otherwise it must satisfy the object key rules without a trailing slash. An invalid directory fails before reading the upload. Unknown or octet-stream content gets no extension. HTML disguised as photo.png can get .html, so sniffing alone does not make unrestricted static serving safe.

    If your application needs a stable replacement key, use PutFileAs. Here userID is the authorized, application-issued portable profile identifier, and the preceding PNG policy has succeeded:

    info, err := files.PutFileAs(r.Context(), "avatars/"+userID+".png", header)

    This replaces exactly that key. Choose the extension from your accepted format, never header.Filename. Both helpers open and close their read-only multipart source themselves, passing its seekable file to the driver. Source close is best-effort and cannot undo or change a successful write’s result; do not repeat the write to compensate for source cleanup.

    Returned ObjectInfo has a key, size, and modification time, not a stored MIME type or original filename. Keep header.Filename separately if your application needs a display name. Serve untrusted objects through Download, Inline, or an isolated origin rather than an unrestricted static route on your app origin.

    Raw Uploads

    For a client that sends bytes directly, Put accepts r.Body. Before this fragment, enforce a body limit, authorize the target, and validate content according to your application’s format policy. Here key is that record’s application-owned key, not an unchecked path value:

    info, err := files.Put(r.Context(), key, r.Body)

    Put does not detect content or close the reader; the HTTP server closes the request body after the handler returns. Reading beyond an enforced body limit fails the reader, so no partial logical object replaces the prior one. Other failures can occur after replacement, as explained in Writing Objects.

    S3 buffers a non-seekable body fully in memory. Keep this path bounded and small, use a seekable multipart source, or choose a direct signed upload for large objects.

    Cancellation is cooperative after work begins; it cannot force an arbitrary blocked reader to return. Keep the request context attached to the operation.

    Serving Files

    Load a document by your application’s identifier and verify that the current user may read it. Do not expose an arbitrary requested storage key. With an authorized document of this shape:

    type StoredDocument struct {
    	Key          string
    	OriginalName string
    	OwnerID      string
    }

    Return the download helper’s result from your handler:

    return files.Download(w, r, document.Key, document.OriginalName)

    The helper opens and always closes the object body. It sets attachment disposition so the browser offers a download. An empty display name uses the key’s last segment; supplied names get an ASCII fallback and UTF-8 presentation where needed. The display name changes presentation, not the stored key.

    Headers and Ranges

    The response MIME type comes from the display filename’s extension, falling back to the first 512 bytes when the extension gives no type. A recognized extension therefore takes precedence over sniffing. Choose trustworthy display metadata and validate accepted formats; the helper adds X-Content-Type-Options: nosniff but does not validate an entire payload.

    Memory and filesystem bodies can seek, so the helper uses http.ServeContent: range requests can return 206, HEAD omits bytes, and Last-Modified with If-Modified-Since can produce 304. Native handling also answers invalid ranges and failed size probes with its own HTTP error responses.

    S3 bodies cannot seek. The helper streams the whole object with its known Content-Length; HEAD receives headers only. It does not process ranges or conditional requests for that body. Use a signed GET when clients need direct S3 range downloads.

    Inline Content

    For an authorized avatar, Inline requests browser display instead of saving:

    return files.Inline(w, r, document.Key)

    Every inline object response includes Content-Security-Policy: sandbox. Only PNG, JPEG, GIF, WebP, AVIF, BMP, ICO, PDF, audio, video, and plain text are allowed inline. Other types, including HTML, SVG, XML, JavaScript, CSS, WebAssembly, JSON, and unknown content, are forced to attachment disposition. The offered name comes from the key’s final segment.

    These restrictions reduce active-content risk on your origin; they are not authorization or content inspection. Use an isolated origin without application cookies when your threat model requires stronger separation.

    Transfer Errors

    A missing object returns storage.ErrObjectNotFound, a 404 problem that also wraps storage.ErrNotFound. Open failures and initial sniff/rewind failures return before response commitment, so the router can still render an error.

    Source, seek, write, short-write, and body-close failures remain discoverable through errors.Is; cleanup errors join transfer errors. Premature EOF during a seekable transfer matches io.ErrUnexpectedEOF. Native ServeContent error responses are retained, not replaced by a problem body.

    An error after commitment does not always mean truncation: a copy failure can truncate the body, while a close failure can arrive after every byte was sent. Return errors for logging, but never append another response or automatically retry a committed transfer. See Committed Failures for that HTTP boundary.

    For all helper options, see the storage API reference.

    Open a bulk documentation archive?

    This file contains every guide in its scope, not API declarations. Loading it all into an agent can exhaust its context window and increase processing time and cost. For coding tasks, start with the index and read only the relevant pages.

    Use the full archive for offline indexing, bulk processing, or deliberate large-context use.

    Browse the index instead
    Open archive