diff --git a/rust/noosphere-core/src/context/content/write.rs b/rust/noosphere-core/src/context/content/write.rs index e13bf387b..b35e6387c 100644 --- a/rust/noosphere-core/src/context/content/write.rs +++ b/rust/noosphere-core/src/context/content/write.rs @@ -158,7 +158,18 @@ where let body_cid = BodyChunkIpld::store_bytes(&bytes, self.sphere_context_mut().await?.db_mut()).await?; - self.link(slug, content_type, &body_cid, additional_headers) + let header = ( + Header::ContentLength.to_string(), + format!("{}", bytes.len()), + ); + let additional_headers = if let Some(mut headers) = additional_headers { + headers.push(header); + headers + } else { + vec![header] + }; + + self.link(slug, content_type, &body_cid, Some(additional_headers)) .await } diff --git a/rust/noosphere-core/src/context/context.rs b/rust/noosphere-core/src/context/context.rs index c8f62e1f6..98c25d4ec 100644 --- a/rust/noosphere-core/src/context/context.rs +++ b/rust/noosphere-core/src/context/context.rs @@ -256,22 +256,82 @@ mod tests { use crate::{ authority::{generate_capability, generate_ed25519_key, Access, SphereAbility}, context::{ - HasMutableSphereContext, HasSphereContext, SphereContentWrite, SpherePetnameWrite, + AsyncFileBody, HasMutableSphereContext, HasSphereContext, SphereContentWrite, + SpherePetnameWrite, }, - data::{ContentType, LinkRecord, LINK_RECORD_FACT_NAME}, + data::{ContentType, Header, LinkRecord, LINK_RECORD_FACT_NAME}, helpers::{make_valid_link_record, simulated_sphere_context}, tracing::initialize_tracing, view::Sphere, }; - use noosphere_storage::{MemoryStorage, SphereDb}; + use noosphere_storage::{MemoryStorage, SphereDb, Storage}; + use serde_json::json; use ucan::{builder::UcanBuilder, crypto::KeyMaterial, store::UcanJwtStore}; + #[cfg(target_arch = "wasm32")] use wasm_bindgen_test::wasm_bindgen_test; #[cfg(target_arch = "wasm32")] wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] + #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] + async fn it_sets_content_length_and_type_on_write() -> Result<()> { + initialize_tracing(None); + + let (mut sphere_context, _) = simulated_sphere_context(Access::ReadWrite, None).await?; + + check_headers( + &mut sphere_context, + "note", + ContentType::Text, + "hello".as_ref(), + "5", + ) + .await?; + + check_headers( + &mut sphere_context, + "note", + ContentType::Json, + json!({"foo":true}).to_string().as_ref(), + "12", + ) + .await?; + + async fn check_headers( + sphere_context: &mut C, + slug: &str, + content_type: ContentType, + content: F, + expected_length: &str, + ) -> Result<()> + where + C: HasMutableSphereContext, + S: Storage + 'static, + F: AsyncFileBody, + { + let db = sphere_context.sphere_context().await?.db().clone(); + let link = sphere_context + .write(slug, &content_type, content, None) + .await?; + let memo = link.load_from(&db).await?; + assert_eq!( + memo.headers, + vec![ + ( + Header::ContentLength.to_string(), + expected_length.to_owned() + ), + (Header::ContentType.to_string(), content_type.to_string()) + ] + ); + Ok(()) + } + Ok(()) + } + #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)] #[cfg_attr(not(target_arch = "wasm32"), tokio::test)] async fn it_validates_slug_names_when_writing() -> Result<()> { diff --git a/rust/noosphere-core/src/data/headers/header.rs b/rust/noosphere-core/src/data/headers/header.rs index aa00f1121..8965d3d17 100644 --- a/rust/noosphere-core/src/data/headers/header.rs +++ b/rust/noosphere-core/src/data/headers/header.rs @@ -2,6 +2,8 @@ use std::{convert::Infallible, fmt::Display, ops::Deref, str::FromStr}; /// Well-known headers in the Noosphere pub enum Header { + /// The content length (in bytes) of associated binary data + ContentLength, /// Content-type, for mimes ContentType, /// A proof, typically a UCAN JWT @@ -34,6 +36,7 @@ impl FromStr for Header { fn from_str(s: &str) -> Result { Ok(match s.to_lowercase().as_str() { + "content-length" => Header::ContentLength, "content-type" => Header::ContentType, "file-extension" => Header::FileExtension, "proof" => Header::Proof, @@ -52,6 +55,7 @@ impl Deref for Header { fn deref(&self) -> &Self::Target { match self { + Header::ContentLength => "Content-Length", Header::ContentType => "Content-Type", Header::Proof => "Proof", Header::Author => "Author", diff --git a/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift b/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift index c9d001d58..53a63a103 100644 --- a/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift +++ b/swift/Tests/SwiftNoosphereTests/NoosphereTests.swift @@ -188,11 +188,12 @@ final class NoosphereTests: XCTestCase { var pointer = file_header_names.ptr!; // NOTE: "hello" is only given once even though there are two headers with that name - assert(name_count == 3) + assert(name_count == 4) let expected_headers = [ ["foo", "bar"], ["hello", "world"], + ["Content-Length", String(file_bytes.count)], ["Content-Type", "text/subtext"] ]