diff --git a/bundler/Cargo.lock b/bundler/Cargo.lock index eafe1d71..50d65955 100644 --- a/bundler/Cargo.lock +++ b/bundler/Cargo.lock @@ -179,6 +179,28 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-extra" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523ae92256049a3b02d3bb4df80152386cd97ddba0c8c5077619bdc8c4b1859b" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -242,10 +264,12 @@ version = "0.1.0" dependencies = [ "anyhow", "axum", + "axum-extra", "built", "clap", "dotenvy", "flate2", + "headers", "humantime", "serde", "serde_json", @@ -667,6 +691,30 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" diff --git a/bundler/Cargo.toml b/bundler/Cargo.toml index eeea7afd..79db2e2a 100644 --- a/bundler/Cargo.toml +++ b/bundler/Cargo.toml @@ -6,9 +6,11 @@ edition = "2021" [dependencies] anyhow = "1.0.75" axum = { version = "0.7.2" } +axum-extra = { version = "0.9.0", features = ["typed-header"] } clap = { version = "4.4.11", features = ["derive", "env"] } dotenvy = { version = "0.15.7" } flate2 = { version = "1.0.28" } +headers = { version = "0.4.0" } humantime = { version = "2.1.0" } serde = { version = "1.0.193", features = ["derive"] } serde_json = { version = "1.0.108" } diff --git a/bundler/src/bundle.rs b/bundler/src/bundle.rs index 0b4fa51d..8f2c99a6 100644 --- a/bundler/src/bundle.rs +++ b/bundler/src/bundle.rs @@ -79,6 +79,10 @@ where Ok(Self::new(metadata, proposals, sessions)) } + pub fn revision(&self) -> &str { + &self.manifest.revision + } + pub fn to_tar_gz(&self) -> Result, anyhow::Error> { let mut bundle_builder = tar::Builder::new(GzEncoder::new(Vec::new(), Compression::best())); diff --git a/bundler/src/main.rs b/bundler/src/main.rs index 785b043f..460d3f5d 100644 --- a/bundler/src/main.rs +++ b/bundler/src/main.rs @@ -3,15 +3,24 @@ mod bundle; mod permissionables; use crate::bundle::{Bundle, NoMetadata}; -use axum::{body::Bytes, extract::State, routing::get, serve, Router}; +use axum::{ + body::Bytes, + extract::State, + http::{HeaderMap, StatusCode}, + response::IntoResponse, + routing::get, + serve, Router, +}; +use axum_extra::TypedHeader; use clap::Parser; +use headers::{ETag, HeaderMapExt, IfNoneMatch}; use serde::Serialize; use sqlx::{mysql::MySqlPoolOptions, MySqlPool}; use std::{ hash::Hash, - marker::PhantomData, net::{Ipv4Addr, SocketAddr, SocketAddrV4}, ops::Add, + str::FromStr, sync::Arc, time::Duration, }; @@ -28,8 +37,8 @@ struct BundleFile where Metadata: Serialize, { + bundle: Bundle, file: Bytes, - _metadata: PhantomData, } impl TryFrom> for BundleFile @@ -41,7 +50,7 @@ where fn try_from(bundle: Bundle) -> Result { Ok(Self { file: bundle.to_tar_gz()?.into(), - _metadata: PhantomData, + bundle, }) } } @@ -115,6 +124,25 @@ async fn update_bundle( } } -async fn bundle_endpoint(State(current_bundle): State) -> Bytes { - current_bundle.as_ref().read().await.file.clone() +async fn bundle_endpoint( + State(current_bundle): State, + if_none_match: Option>, +) -> impl IntoResponse { + let etag = ETag::from_str(&format!( + r#""{}""#, + current_bundle.as_ref().read().await.bundle.revision() + )) + .unwrap(); + let mut headers = HeaderMap::new(); + headers.typed_insert(etag.clone()); + match if_none_match { + Some(TypedHeader(if_none_match)) if !if_none_match.precondition_passes(&etag) => { + (StatusCode::NOT_MODIFIED, headers, Bytes::new()) + } + _ => ( + StatusCode::OK, + headers, + current_bundle.as_ref().read().await.file.clone(), + ), + } }