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:
parent
309763397f
commit
143be85416
10 changed files with 56 additions and 38 deletions
crates/treehouse
src
tests/it/vfs
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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());
|
||||
|
|
Loading…
Reference in a new issue