Skip to content

Commit

Permalink
Add Cache-Control header to asset endpoint
Browse files Browse the repository at this point in the history
resolves #11
  • Loading branch information
ackwell committed Aug 1, 2024
1 parent 59a2c38 commit 4b8d9bb
Show file tree
Hide file tree
Showing 3 changed files with 33 additions and 8 deletions.
3 changes: 3 additions & 0 deletions boilmaster.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ port = 8080
username = "username"
password = "password"

[http.api1.asset]
maxage = 604800 # 1 week

[http.api1.search]
limit.default = 100
limit.max = 500
Expand Down
3 changes: 2 additions & 1 deletion src/http/api1/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const OPENAPI_JSON_ROUTE: &str = "/openapi.json";

#[derive(Debug, Deserialize)]
pub struct Config {
asset: asset::Config,
search: search::Config,
sheet: sheet::Config,
}
Expand All @@ -30,7 +31,7 @@ pub fn router(config: Config) -> Router<service::State> {
ApiRouter::new()
.nest(
"/asset",
asset::router().with_path_items(|item| item.tag("assets")),
asset::router(config.asset).with_path_items(|item| item.tag("assets")),
)
.nest(
"/search",
Expand Down
35 changes: 28 additions & 7 deletions src/http/api1/asset.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::{
ffi::OsStr,
hash::{Hash, Hasher},
time::Duration,
};

use aide::{
Expand All @@ -9,9 +10,9 @@ use aide::{
transform::TransformOperation,
NoApi,
};
use axum::{debug_handler, extract::State, http::header, response::IntoResponse};
use axum::{debug_handler, extract::State, http::header, response::IntoResponse, Extension};
use axum_extra::{
headers::{ContentType, ETag, IfNoneMatch},
headers::{CacheControl, ContentType, ETag, IfNoneMatch},
TypedHeader,
};
use reqwest::StatusCode;
Expand All @@ -30,8 +31,15 @@ use super::{
// NOTE: Bump this if changing any behavior that impacts output binary data for assets, to ensure ETag is cache-broken.
const ASSET_ETAG_VERSION: usize = 2;

pub fn router() -> ApiRouter<service::State> {
ApiRouter::new().api_route("/*path", get_with(asset, asset_docs))
#[derive(Debug, Clone, Deserialize)]
pub struct Config {
maxage: u64,
}

pub fn router(config: Config) -> ApiRouter<service::State> {
ApiRouter::new()
.api_route("/*path", get_with(asset, asset_docs))
.layer(Extension(config))
}

/// Path variables accepted by the asset endpoint.
Expand Down Expand Up @@ -83,33 +91,46 @@ async fn asset(
Query(query): Query<AssetQuery>,
NoApi(header_if_none_match): NoApi<Option<TypedHeader<IfNoneMatch>>>,
State(asset): State<service::Asset>,
Extension(config): Extension<Config>,
) -> Result<impl IntoApiResponse> {
let format = query.format;

let etag = etag(&path, format, version_key);

// If the request came through with a passing ETag, we can skip doing any processing.
if let Some(TypedHeader(if_none_match)) = header_if_none_match {
if !if_none_match.precondition_passes(&etag) {
return Ok(StatusCode::NOT_MODIFIED.into_response());
}
}

// Perform the conversion.
// TODO: can this be made async?
let bytes = asset.convert(version_key, &path, format)?;

// Try to derive a filename to use for the Content-Disposition header.
let filepath = std::path::Path::new(&path).with_extension(format.extension());
let disposition = match filepath.file_name().and_then(OsStr::to_str) {
Some(name) => format!("inline; filename=\"{name}\""),
None => "inline".to_string(),
};

Ok((
// Set up the Cache-Control header based on configured max-age.
let cache_control = CacheControl::new()
.with_public()
.with_immutable()
.with_max_age(Duration::from_secs(config.maxage));

let response = (
TypedHeader(ContentType::from(format_mime(format))),
// TypedHeader only has a really naive inline value with no ability to customise :/
[(header::CONTENT_DISPOSITION, disposition)],
TypedHeader(etag),
TypedHeader(cache_control),
bytes,
)
.into_response())
);

Ok(response.into_response())
}

fn format_mime(format: Format) -> mime::Mime {
Expand Down

0 comments on commit 4b8d9bb

Please sign in to comment.