Skip to main content

serve

Function serve 

Source
pub fn serve(
    ctx: &Ctx,
    attachment: &Attachment,
    headers: &HeaderMap,
    disposition: Disposition,
) -> impl Future<Output = Result<Response>> + Send + use<>
Expand description

Streams a stored file to the client, with the headers a browser needs for caching, seeking and saving it.

The response carries Content-Type, Content-Length, Content-Disposition (the original file name; inline only for types safe to display, see Disposition), ETag and Cache-Control (CACHE_CONTROL). A matching If-None-Match answers 304 Not Modified without a body; a single Range (video seeking, resumed downloads) answers 206 with Content-Range, or 416 when it lies outside the file (without calling R2). Several ranges, other units and malformed values send the whole file, as RFC 9110 allows. The headers come from attachment, so it must be the row saved for that key.

Check that the user may see the record before calling it: the route is the only protection.

Free plan: one R2 class B operation per call, 304s included (10M free per month). The bytes never pass through WebAssembly: the R2 stream is attached to the response and crate::serve answers with it directly, so a download costs almost no CPU whatever its size.

§Errors

  • Error::NotFound (404) when no object has attachment.key.
  • Error::Internal (500) when the STORAGE binding is missing (the message shows the STORAGE entry to add to cloudflare.config.ts) or R2 fails.

§Examples

use axum::{extract::{Path, Query, State}, http::HeaderMap, response::Response};
use ocre::storage::{self, Attachment, Disposition};
use ocre::{Ctx, OptionExt, Result, params};
use serde::Deserialize;

#[derive(Deserialize)]
struct Download {
    #[serde(default)]
    download: bool,
}

// GET /photos/{id}/image, or /photos/{id}/image?download=true for "Save as".
async fn image(
    State(ctx): State<Ctx>,
    Path(id): Path<i64>,
    Query(query): Query<Download>,
    headers: HeaderMap,
) -> Result<Response> {
    let sql = "SELECT image_key AS key, image_filename AS filename, image_content_type AS content_type, \
               image_size AS size FROM photos WHERE id = ?1";
    let image: Attachment = ctx.db()?.first(sql, params![id]).await?.or_404()?;
    let disposition = if query.download { Disposition::Download } else { Disposition::Inline };
    storage::serve(&ctx, &image, &headers, disposition).await
}