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 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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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]
|
||||||
|
|
|
@ -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());
|
||||||
|
|
Loading…
Reference in a new issue