diff --git a/crates/treehouse/src/cli/serve.rs b/crates/treehouse/src/cli/serve.rs index 4f7fa5f..4f2f0db 100644 --- a/crates/treehouse/src/cli/serve.rs +++ b/crates/treehouse/src/cli/serve.rs @@ -18,7 +18,7 @@ use axum::{ }; use serde::Deserialize; use tokio::net::TcpListener; -use tracing::{info, instrument}; +use tracing::{error, info, instrument}; use crate::dirs::Dirs; use crate::sources::Sources; @@ -67,17 +67,6 @@ pub async fn serve( Ok(axum::serve(listener, app).await?) } -fn get_content_type(extension: &str) -> Option<&'static str> { - match extension { - "html" => Some("text/html"), - "js" => Some("text/javascript"), - "woff" => Some("font/woff2"), - "svg" => Some("image/svg+xml"), - "atom" => Some("application/atom+xml"), - _ => None, - } -} - #[derive(Debug, Deserialize)] struct VfsQuery { #[serde(rename = "v")] @@ -87,16 +76,12 @@ struct VfsQuery { #[instrument(skip(state))] async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> { let vpath = VPath::try_new(path).ok()?; - let content = state.target.content(vpath).await.map(|c| c.bytes())?; - let mut response = content.into_response(); - - if let Some(content_type) = vpath.extension().and_then(get_content_type) { - response - .headers_mut() - .insert(CONTENT_TYPE, HeaderValue::from_static(content_type)); - } else { - response.headers_mut().remove(CONTENT_TYPE); - } + let content = state.target.content(vpath).await?; + let content_type = HeaderValue::from_str(content.kind()).inspect_err( + |err| error!(?err, content_type = ?content.kind(), "content type cannot be used as an HTTP header"), + ).ok()?; + let mut response = content.bytes().into_response(); + response.headers_mut().insert(CONTENT_TYPE, content_type); if query.content_version.is_some() { response.headers_mut().insert( diff --git a/crates/treehouse/src/generate.rs b/crates/treehouse/src/generate.rs index 547e77e..ec43d98 100644 --- a/crates/treehouse/src/generate.rs +++ b/crates/treehouse/src/generate.rs @@ -134,6 +134,7 @@ impl TreehouseDir { .get(path) .map(|&file_id| { Content::new( + "text/html", tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id) .into(), ) @@ -143,6 +144,7 @@ impl TreehouseDir { let template_name = path.with_extension("hbs"); if self.handlebars.has_template(template_name.as_str()) { return Some(Content::new( + "text/html", simple_template::generate_or_error( &self.sources, &self.handlebars, diff --git a/crates/treehouse/src/generate/atom.rs b/crates/treehouse/src/generate/atom.rs index 9f0d9c5..6223253 100644 --- a/crates/treehouse/src/generate/atom.rs +++ b/crates/treehouse/src/generate/atom.rs @@ -51,7 +51,6 @@ impl FeedDir { } fn content(&self, path: &VPath) -> Option<Content> { - info!("{path}"); if path.extension() == Some("atom") { let feed_name = path.with_extension("").to_string(); self.sources @@ -60,6 +59,7 @@ impl FeedDir { .get(&feed_name) .map(|file_id| { Content::new( + "application/atom+xml", generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id) .into(), ) diff --git a/crates/treehouse/src/main.rs b/crates/treehouse/src/main.rs index bfe9b7e..e8dcde1 100644 --- a/crates/treehouse/src/main.rs +++ b/crates/treehouse/src/main.rs @@ -13,7 +13,7 @@ use treehouse::generate; use treehouse::sources::Sources; use treehouse::vfs::asynch::AsyncDir; use treehouse::vfs::{ - AnchoredAtExt, Blake3ContentVersionCache, DynDir, ImageSizeCache, ToDynDir, VPathBuf, + AnchoredAtExt, Blake3ContentVersionCache, Content, DynDir, ImageSizeCache, ToDynDir, VPathBuf, }; use treehouse::vfs::{Cd, PhysicalDir}; use treehouse::{ @@ -30,7 +30,11 @@ fn vfs_sources() -> anyhow::Result<DynDir> { root.add( VPath::new("treehouse.toml"), - BufferedFile::new(fs::read("treehouse.toml")?).to_dyn(), + BufferedFile::new(Content::new( + "application/toml", + fs::read("treehouse.toml")?, + )) + .to_dyn(), ); root.add( VPath::new("static"), diff --git a/crates/treehouse/src/vfs.rs b/crates/treehouse/src/vfs.rs index d121274..7d9c6b4 100644 --- a/crates/treehouse/src/vfs.rs +++ b/crates/treehouse/src/vfs.rs @@ -239,6 +239,8 @@ pub struct Entries(pub Vec<VPathBuf>); /// Byte content in an entry. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Content { + /// Media type string. <https://en.wikipedia.org/wiki/Media_type> + kind: String, bytes: Vec<u8>, } @@ -262,8 +264,15 @@ pub struct ImageSize { } impl Content { - pub fn new(bytes: Vec<u8>) -> Self { - Self { bytes } + pub fn new(kind: impl Into<String>, bytes: Vec<u8>) -> Self { + Self { + kind: kind.into(), + bytes, + } + } + + pub fn kind(&self) -> &str { + &self.kind } pub fn bytes(self) -> Vec<u8> { diff --git a/crates/treehouse/src/vfs/file.rs b/crates/treehouse/src/vfs/file.rs index 19fa3cd..c492e9d 100644 --- a/crates/treehouse/src/vfs/file.rs +++ b/crates/treehouse/src/vfs/file.rs @@ -3,11 +3,11 @@ use std::fmt; use super::{Content, Dir, Query, VPath}; pub struct BufferedFile { - pub content: Vec<u8>, + pub content: Content, } impl BufferedFile { - pub fn new(content: Vec<u8>) -> Self { + pub fn new(content: Content) -> Self { Self { content } } } @@ -15,7 +15,7 @@ impl BufferedFile { impl Dir for BufferedFile { fn query(&self, path: &VPath, query: &mut Query) { if path == VPath::ROOT { - query.provide(|| Content::new(self.content.clone())); + query.provide(|| self.content.clone()); } } } diff --git a/crates/treehouse/src/vfs/physical.rs b/crates/treehouse/src/vfs/physical.rs index 4835f47..871a044 100644 --- a/crates/treehouse/src/vfs/physical.rs +++ b/crates/treehouse/src/vfs/physical.rs @@ -62,7 +62,14 @@ impl PhysicalDir { std::fs::read(self.root.join(physical_path(path))) .inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?}: {err:?}",)) .ok() - .map(Content::new) + .map(|bytes| { + Content::new( + path.extension() + .and_then(guess_content_type) + .unwrap_or("text/plain"), + bytes, + ) + }) } fn edit_path(&self, path: &VPath) -> EditPath { @@ -83,3 +90,14 @@ impl Dir for PhysicalDir { fn physical_path(path: &VPath) -> &Path { Path::new(path.as_str()) } + +fn guess_content_type(extension: &str) -> Option<&'static str> { + match extension { + "html" => Some("text/html"), + "js" => Some("text/javascript"), + "woff" => Some("font/woff2"), + "svg" => Some("image/svg+xml"), + "atom" => Some("application/atom+xml"), + _ => None, + } +} diff --git a/crates/treehouse/tests/it/vfs/cd.rs b/crates/treehouse/tests/it/vfs/cd.rs index cad2583..3c7c0b7 100644 --- a/crates/treehouse/tests/it/vfs/cd.rs +++ b/crates/treehouse/tests/it/vfs/cd.rs @@ -7,9 +7,9 @@ const FWOOFEE: &[u8] = b"fwoofee -w-"; const BOOP: &[u8] = b"boop >w<"; fn vfs() -> MemDir { - let file1 = BufferedFile::new(HEWWO.to_vec()); - let file2 = BufferedFile::new(FWOOFEE.to_vec()); - let file3 = BufferedFile::new(BOOP.to_vec()); + let file1 = BufferedFile::new(Content::new("text/plain", HEWWO.to_vec())); + let file2 = BufferedFile::new(Content::new("text/plain", FWOOFEE.to_vec())); + let file3 = BufferedFile::new(Content::new("text/plain", BOOP.to_vec())); let mut innermost = MemDir::new(); innermost.add(VPath::new("file3.txt"), file3.to_dyn()); diff --git a/crates/treehouse/tests/it/vfs/file.rs b/crates/treehouse/tests/it/vfs/file.rs index 077b51a..2085a7f 100644 --- a/crates/treehouse/tests/it/vfs/file.rs +++ b/crates/treehouse/tests/it/vfs/file.rs @@ -1,7 +1,7 @@ use treehouse::vfs::{entries, query, BufferedFile, Content, VPath}; fn vfs() -> BufferedFile { - BufferedFile::new(b"hewwo :3".to_vec()) + BufferedFile::new(Content::new("text/plain", b"hewwo :3".to_vec())) } #[test] diff --git a/crates/treehouse/tests/it/vfs/mount_points.rs b/crates/treehouse/tests/it/vfs/mount_points.rs index c030784..fbd3214 100644 --- a/crates/treehouse/tests/it/vfs/mount_points.rs +++ b/crates/treehouse/tests/it/vfs/mount_points.rs @@ -5,9 +5,9 @@ const FWOOFEE: &[u8] = b"fwoofee -w-"; const BOOP: &[u8] = b"boop >w<"; fn vfs() -> MemDir { - let file1 = BufferedFile::new(HEWWO.to_vec()); - let file2 = BufferedFile::new(FWOOFEE.to_vec()); - let file3 = BufferedFile::new(BOOP.to_vec()); + let file1 = BufferedFile::new(Content::new("text/plain", HEWWO.to_vec())); + let file2 = BufferedFile::new(Content::new("text/plain", FWOOFEE.to_vec())); + let file3 = BufferedFile::new(Content::new("text/plain", BOOP.to_vec())); let mut inner = MemDir::new(); inner.add(VPath::new("file3.txt"), file3.to_dyn());