1
Fork 0

add explicit content types to VFS

this allows us to serve a precise Content-Type header for all our pages, rendering treehouse usable in browsers that require one
(like terminal browsers---w3m, lynx)
This commit is contained in:
リキ萌 2025-01-14 23:09:01 +01:00
parent 309763397f
commit 143be85416
10 changed files with 56 additions and 38 deletions

View file

@ -18,7 +18,7 @@ use axum::{
}; };
use serde::Deserialize; use serde::Deserialize;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::{info, instrument}; use tracing::{error, info, instrument};
use crate::dirs::Dirs; use crate::dirs::Dirs;
use crate::sources::Sources; use crate::sources::Sources;
@ -67,17 +67,6 @@ pub async fn serve(
Ok(axum::serve(listener, app).await?) 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)] #[derive(Debug, Deserialize)]
struct VfsQuery { struct VfsQuery {
#[serde(rename = "v")] #[serde(rename = "v")]
@ -87,16 +76,12 @@ struct VfsQuery {
#[instrument(skip(state))] #[instrument(skip(state))]
async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> { async fn get_static_file(path: &str, query: &VfsQuery, state: &Server) -> Option<Response> {
let vpath = VPath::try_new(path).ok()?; let vpath = VPath::try_new(path).ok()?;
let content = state.target.content(vpath).await.map(|c| c.bytes())?; let content = state.target.content(vpath).await?;
let mut response = content.into_response(); 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"),
if let Some(content_type) = vpath.extension().and_then(get_content_type) { ).ok()?;
response let mut response = content.bytes().into_response();
.headers_mut() response.headers_mut().insert(CONTENT_TYPE, content_type);
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
} else {
response.headers_mut().remove(CONTENT_TYPE);
}
if query.content_version.is_some() { if query.content_version.is_some() {
response.headers_mut().insert( response.headers_mut().insert(

View file

@ -134,6 +134,7 @@ impl TreehouseDir {
.get(path) .get(path)
.map(|&file_id| { .map(|&file_id| {
Content::new( Content::new(
"text/html",
tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id) tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id)
.into(), .into(),
) )
@ -143,6 +144,7 @@ impl TreehouseDir {
let template_name = path.with_extension("hbs"); let template_name = path.with_extension("hbs");
if self.handlebars.has_template(template_name.as_str()) { if self.handlebars.has_template(template_name.as_str()) {
return Some(Content::new( return Some(Content::new(
"text/html",
simple_template::generate_or_error( simple_template::generate_or_error(
&self.sources, &self.sources,
&self.handlebars, &self.handlebars,

View file

@ -51,7 +51,6 @@ impl FeedDir {
} }
fn content(&self, path: &VPath) -> Option<Content> { fn content(&self, path: &VPath) -> Option<Content> {
info!("{path}");
if path.extension() == Some("atom") { if path.extension() == Some("atom") {
let feed_name = path.with_extension("").to_string(); let feed_name = path.with_extension("").to_string();
self.sources self.sources
@ -60,6 +59,7 @@ impl FeedDir {
.get(&feed_name) .get(&feed_name)
.map(|file_id| { .map(|file_id| {
Content::new( Content::new(
"application/atom+xml",
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id) generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id)
.into(), .into(),
) )

View file

@ -13,7 +13,7 @@ use treehouse::generate;
use treehouse::sources::Sources; use treehouse::sources::Sources;
use treehouse::vfs::asynch::AsyncDir; use treehouse::vfs::asynch::AsyncDir;
use treehouse::vfs::{ use treehouse::vfs::{
AnchoredAtExt, Blake3ContentVersionCache, DynDir, ImageSizeCache, ToDynDir, VPathBuf, AnchoredAtExt, Blake3ContentVersionCache, Content, DynDir, ImageSizeCache, ToDynDir, VPathBuf,
}; };
use treehouse::vfs::{Cd, PhysicalDir}; use treehouse::vfs::{Cd, PhysicalDir};
use treehouse::{ use treehouse::{
@ -30,7 +30,11 @@ fn vfs_sources() -> anyhow::Result<DynDir> {
root.add( root.add(
VPath::new("treehouse.toml"), 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( root.add(
VPath::new("static"), VPath::new("static"),

View file

@ -239,6 +239,8 @@ pub struct Entries(pub Vec<VPathBuf>);
/// Byte content in an entry. /// Byte content in an entry.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Content { pub struct Content {
/// Media type string. <https://en.wikipedia.org/wiki/Media_type>
kind: String,
bytes: Vec<u8>, bytes: Vec<u8>,
} }
@ -262,8 +264,15 @@ pub struct ImageSize {
} }
impl Content { impl Content {
pub fn new(bytes: Vec<u8>) -> Self { pub fn new(kind: impl Into<String>, bytes: Vec<u8>) -> Self {
Self { bytes } Self {
kind: kind.into(),
bytes,
}
}
pub fn kind(&self) -> &str {
&self.kind
} }
pub fn bytes(self) -> Vec<u8> { pub fn bytes(self) -> Vec<u8> {

View file

@ -3,11 +3,11 @@ use std::fmt;
use super::{Content, Dir, Query, VPath}; use super::{Content, Dir, Query, VPath};
pub struct BufferedFile { pub struct BufferedFile {
pub content: Vec<u8>, pub content: Content,
} }
impl BufferedFile { impl BufferedFile {
pub fn new(content: Vec<u8>) -> Self { pub fn new(content: Content) -> Self {
Self { content } Self { content }
} }
} }
@ -15,7 +15,7 @@ impl BufferedFile {
impl Dir for BufferedFile { impl Dir for BufferedFile {
fn query(&self, path: &VPath, query: &mut Query) { fn query(&self, path: &VPath, query: &mut Query) {
if path == VPath::ROOT { if path == VPath::ROOT {
query.provide(|| Content::new(self.content.clone())); query.provide(|| self.content.clone());
} }
} }
} }

View file

@ -62,7 +62,14 @@ impl PhysicalDir {
std::fs::read(self.root.join(physical_path(path))) std::fs::read(self.root.join(physical_path(path)))
.inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?}: {err:?}",)) .inspect_err(|err| error!("{self:?} cannot read file at vpath {path:?}: {err:?}",))
.ok() .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 { fn edit_path(&self, path: &VPath) -> EditPath {
@ -83,3 +90,14 @@ impl Dir for PhysicalDir {
fn physical_path(path: &VPath) -> &Path { fn physical_path(path: &VPath) -> &Path {
Path::new(path.as_str()) 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,
}
}

View file

@ -7,9 +7,9 @@ const FWOOFEE: &[u8] = b"fwoofee -w-";
const BOOP: &[u8] = b"boop >w<"; const BOOP: &[u8] = b"boop >w<";
fn vfs() -> MemDir { fn vfs() -> MemDir {
let file1 = BufferedFile::new(HEWWO.to_vec()); let file1 = BufferedFile::new(Content::new("text/plain", HEWWO.to_vec()));
let file2 = BufferedFile::new(FWOOFEE.to_vec()); let file2 = BufferedFile::new(Content::new("text/plain", FWOOFEE.to_vec()));
let file3 = BufferedFile::new(BOOP.to_vec()); let file3 = BufferedFile::new(Content::new("text/plain", BOOP.to_vec()));
let mut innermost = MemDir::new(); let mut innermost = MemDir::new();
innermost.add(VPath::new("file3.txt"), file3.to_dyn()); innermost.add(VPath::new("file3.txt"), file3.to_dyn());

View file

@ -1,7 +1,7 @@
use treehouse::vfs::{entries, query, BufferedFile, Content, VPath}; use treehouse::vfs::{entries, query, BufferedFile, Content, VPath};
fn vfs() -> BufferedFile { fn vfs() -> BufferedFile {
BufferedFile::new(b"hewwo :3".to_vec()) BufferedFile::new(Content::new("text/plain", b"hewwo :3".to_vec()))
} }
#[test] #[test]

View file

@ -5,9 +5,9 @@ const FWOOFEE: &[u8] = b"fwoofee -w-";
const BOOP: &[u8] = b"boop >w<"; const BOOP: &[u8] = b"boop >w<";
fn vfs() -> MemDir { fn vfs() -> MemDir {
let file1 = BufferedFile::new(HEWWO.to_vec()); let file1 = BufferedFile::new(Content::new("text/plain", HEWWO.to_vec()));
let file2 = BufferedFile::new(FWOOFEE.to_vec()); let file2 = BufferedFile::new(Content::new("text/plain", FWOOFEE.to_vec()));
let file3 = BufferedFile::new(BOOP.to_vec()); let file3 = BufferedFile::new(Content::new("text/plain", BOOP.to_vec()));
let mut inner = MemDir::new(); let mut inner = MemDir::new();
inner.add(VPath::new("file3.txt"), file3.to_dyn()); inner.add(VPath::new("file3.txt"), file3.to_dyn());