diff options
-rw-r--r-- | kittybox-rs/src/media/mod.rs | 31 | ||||
-rw-r--r-- | kittybox-rs/src/media/storage/file.rs | 6 | ||||
-rw-r--r-- | kittybox-rs/src/media/storage/mod.rs | 5 |
3 files changed, 38 insertions, 4 deletions
diff --git a/kittybox-rs/src/media/mod.rs b/kittybox-rs/src/media/mod.rs index 0ce2ec9..ce08704 100644 --- a/kittybox-rs/src/media/mod.rs +++ b/kittybox-rs/src/media/mod.rs @@ -1,6 +1,10 @@ +use std::convert::TryFrom; + use axum::{ extract::{Extension, Host, multipart::Multipart, Path}, - response::{IntoResponse, Response}, headers::HeaderValue, + response::{IntoResponse, Response}, + headers::{Header, HeaderValue, IfNoneMatch, HeaderMapExt}, + TypedHeader, }; use kittybox_util::error::{MicropubError, ErrorType}; use kittybox_indieauth::Scope; @@ -65,6 +69,7 @@ pub(crate) async fn upload<S: MediaStore, A: AuthBackend>( pub(crate) async fn serve<S: MediaStore>( Host(host): Host, Path(path): Path<String>, + if_none_match: Option<TypedHeader<IfNoneMatch>>, Extension(blobstore): Extension<S> ) -> Response { use axum::http::StatusCode; @@ -72,6 +77,23 @@ pub(crate) async fn serve<S: MediaStore>( match blobstore.read_streaming(&host, path.as_str()).await { Ok((metadata, stream)) => { tracing::debug!("Metadata: {:?}", metadata); + + let etag = if let Some(etag) = metadata.etag { + let etag = format!("\"{}\"", etag).parse::<axum::headers::ETag>().unwrap(); + + if let Some(TypedHeader(if_none_match)) = if_none_match { + tracing::debug!("If-None-Match: {:?}", if_none_match); + // If-None-Match is a negative precondition that + // returns 304 when it doesn't match because it + // only matches when file is different + if !if_none_match.precondition_passes(&etag) { + return StatusCode::NOT_MODIFIED.into_response() + } + } + + Some(etag) + } else { None }; + let mut r = Response::builder(); { let headers = r.headers_mut().unwrap(); @@ -89,8 +111,13 @@ pub(crate) async fn serve<S: MediaStore>( HeaderValue::from_str(&length.to_string()).unwrap() ); } + if let Some(etag) = etag { + headers.typed_insert(etag); + } } - r.body(axum::body::StreamBody::new(stream)).unwrap().into_response() + r.body(axum::body::StreamBody::new(stream)) + .unwrap() + .into_response() }, Err(err) => match err.kind() { ErrorKind::NotFound => { diff --git a/kittybox-rs/src/media/storage/file.rs b/kittybox-rs/src/media/storage/file.rs index 1e0ff0e..42149aa 100644 --- a/kittybox-rs/src/media/storage/file.rs +++ b/kittybox-rs/src/media/storage/file.rs @@ -113,6 +113,7 @@ impl MediaStore for FileStore { let metapath = self.base.join(domain_str.as_str()).join(&metafilename); let metatemppath = self.base.join(domain_str.as_str()).join(metafilename + ".tmp"); metadata.length = std::num::NonZeroUsize::new(length); + metadata.etag = Some(hex::encode(&hash)); debug!("File path: {}, metadata: {}", filepath.display(), metapath.display()); { let parent = filepath.parent().unwrap(); @@ -188,7 +189,8 @@ mod tests { let metadata = Metadata { filename: Some("style.css".to_string()), content_type: Some("text/css".to_string()), - length: None + length: None, + etag: None, }; // write through the interface @@ -216,6 +218,7 @@ mod tests { assert_eq!(meta.content_type.as_deref(), Some("text/css")); assert_eq!(meta.filename.as_deref(), Some("style.css")); assert_eq!(meta.length.map(|i| i.get()), Some(file.len())); + assert!(meta.etag.is_some()); // read back the data using the interface let (metadata, read_back) = { @@ -235,6 +238,7 @@ mod tests { assert_eq!(metadata.content_type.as_deref(), Some("text/css")); assert_eq!(meta.filename.as_deref(), Some("style.css")); assert_eq!(meta.length.map(|i| i.get()), Some(file.len())); + assert!(meta.etag.is_some()); } } diff --git a/kittybox-rs/src/media/storage/mod.rs b/kittybox-rs/src/media/storage/mod.rs index b34da88..4ef7c7a 100644 --- a/kittybox-rs/src/media/storage/mod.rs +++ b/kittybox-rs/src/media/storage/mod.rs @@ -17,6 +17,8 @@ pub struct Metadata { pub filename: Option<String>, /// The recorded length of the file. pub length: Option<NonZeroUsize>, + /// The e-tag of a file. Note: it must be a strong e-tag, for example, a hash. + pub etag: Option<String>, } impl From<&Field<'_>> for Metadata { fn from(field: &Field<'_>) -> Self { @@ -25,7 +27,8 @@ impl From<&Field<'_>> for Metadata { .map(|i| i.to_owned()), filename: field.file_name() .map(|i| i.to_owned()), - length: None + length: None, + etag: None, } } } |