about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2022-10-04 23:55:44 +0300
committerVika <vika@fireburn.ru>2022-10-04 23:55:44 +0300
commit6cb479acc61ab19f655cedd878283b214e352a3d (patch)
tree3fd47359714f4e7bac3f52d45e199a0ae5bc829b
parent0e0d711a9d524c445a61a05831a824ac7080f3b8 (diff)
downloadkittybox-6cb479acc61ab19f655cedd878283b214e352a3d.tar.zst
media: Use ETag and If-None-Match
Note: this requires a reindex of the media database. For the default
CAS backend, use the following:

```bash
for i in */*/*/*/*.json; do
    etag="$(echo $i | sed -e 's/\///g' -e 's/\.json$//')";
    mv "$i" "$i.bak"
    cat "$i.bak" | jq '. + { "etag": '\""$etag"\"'}' > "$i"
    rm "$i.bak"
done
```

This change is backwards compatible, but caching headers won't be
emitted without etags present in the metadata.

Actual etags are backend-specific and might differ from backend to
backend.
-rw-r--r--kittybox-rs/src/media/mod.rs31
-rw-r--r--kittybox-rs/src/media/storage/file.rs6
-rw-r--r--kittybox-rs/src/media/storage/mod.rs5
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,
         }
     }
 }