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 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(

View file

@ -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,

View file

@ -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(),
)

View file

@ -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"),

View file

@ -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> {

View file

@ -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());
}
}
}

View file

@ -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,
}
}

View file

@ -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());

View file

@ -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]

View file

@ -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());