change vfs API to something akin to the deleted std provider API
this gives us a _much_ easier time with composing file systems, since you can call `query` on the inner file system and all the magic will be done for you the overhead is technically slightly higher, but seems to be drowned out by other more expensive stuff; i couldn't measure a difference in my traces
This commit is contained in:
parent
5b6d637f44
commit
600651ec16
|
@ -8,7 +8,7 @@ use treehouse_format::ast::Branch;
|
|||
use crate::{
|
||||
parse::{self, parse_toml_with_diagnostics, parse_tree_with_diagnostics},
|
||||
state::{report_diagnostics, FileId, Source, Treehouse},
|
||||
vfs::{self, Dir, Edit, VPath},
|
||||
vfs::{self, Content, Dir, Edit, EditPath, VPath},
|
||||
};
|
||||
|
||||
use super::{FixAllArgs, FixArgs};
|
||||
|
@ -138,17 +138,15 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
|
|||
let file = if &*fix_args.file == VPath::new("-") {
|
||||
std::io::read_to_string(std::io::stdin().lock()).context("cannot read file from stdin")?
|
||||
} else {
|
||||
String::from_utf8(
|
||||
root.content(&fix_args.file)
|
||||
.ok_or_else(|| anyhow!("cannot read file to fix"))?,
|
||||
)
|
||||
.context("input file has invalid UTF-8")?
|
||||
vfs::query::<Content>(root, &fix_args.file)
|
||||
.ok_or_else(|| anyhow!("cannot read file to fix"))?
|
||||
.string()?
|
||||
};
|
||||
|
||||
let mut treehouse = Treehouse::new();
|
||||
let mut diagnostics = vec![];
|
||||
let file_id = treehouse.add_file(fix_args.file.clone(), Source::Other(file));
|
||||
let edit_path = root.edit_path(&fix_args.file).ok_or_else(|| {
|
||||
let edit_path = vfs::query::<EditPath>(root, &fix_args.file).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"{} is not an editable file (perhaps it is not in a persistent path?)",
|
||||
fix_args.file
|
||||
|
@ -161,9 +159,10 @@ pub fn fix_file_cli(fix_args: FixArgs, root: &dyn Dir) -> anyhow::Result<Edit> {
|
|||
// Try to write the backup first. If writing that fails, bail out without overwriting
|
||||
// the source file.
|
||||
if let Some(backup_path) = fix_args.backup {
|
||||
let backup_edit_path = root.edit_path(&backup_path).ok_or_else(|| {
|
||||
anyhow!("backup file {backup_path} is not an editable file")
|
||||
})?;
|
||||
let backup_edit_path =
|
||||
vfs::query::<EditPath>(root, &backup_path).ok_or_else(|| {
|
||||
anyhow!("backup file {backup_path} is not an editable file")
|
||||
})?;
|
||||
Edit::Seq(vec![
|
||||
Edit::Write(
|
||||
backup_edit_path,
|
||||
|
@ -190,7 +189,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
|
|||
|
||||
fn fix_one(dir: &dyn Dir, path: &VPath) -> anyhow::Result<Edit> {
|
||||
if path.extension() == Some("tree") {
|
||||
let Some(content) = dir.content(path) else {
|
||||
let Some(content) = vfs::query::<Content>(dir, path).map(Content::bytes) else {
|
||||
return Ok(Edit::NoOp);
|
||||
};
|
||||
let content = String::from_utf8(content).context("file is not valid UTF-8")?;
|
||||
|
@ -198,7 +197,7 @@ pub fn fix_all_cli(fix_all_args: FixAllArgs, dir: &dyn Dir) -> anyhow::Result<Ed
|
|||
let mut treehouse = Treehouse::new();
|
||||
let mut diagnostics = vec![];
|
||||
let file_id = treehouse.add_file(path.to_owned(), Source::Other(content));
|
||||
let edit_path = dir.edit_path(path).context("path is not editable")?;
|
||||
let edit_path = vfs::query::<EditPath>(dir, path).context("path is not editable")?;
|
||||
|
||||
if let Ok(fixed) = fix_file(&mut treehouse, &mut diagnostics, file_id) {
|
||||
if fixed != treehouse.source(file_id).input() {
|
||||
|
|
|
@ -73,7 +73,7 @@ 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?;
|
||||
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) {
|
||||
|
@ -108,7 +108,7 @@ async fn vfs_entry(
|
|||
|
||||
async fn system_page(target: &AsyncDir, path: &VPath, status_code: StatusCode) -> Response {
|
||||
if let Some(content) = target.content(path).await {
|
||||
(status_code, Html(content)).into_response()
|
||||
(status_code, Html(content.bytes())).into_response()
|
||||
} else {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
@ -152,7 +152,7 @@ async fn branch(RawQuery(named_id): RawQuery, State(state): State<Arc<Server>>)
|
|||
.target
|
||||
.content(tree_path)
|
||||
.await
|
||||
.and_then(|s| String::from_utf8(s).ok())
|
||||
.and_then(|c| c.string().ok())
|
||||
{
|
||||
let branch_markup = input[branch.content.clone()].trim();
|
||||
let mut per_page_metadata =
|
||||
|
|
|
@ -5,7 +5,7 @@ use treehouse_format::ast::{Branch, Roots};
|
|||
use crate::{
|
||||
parse::parse_tree_with_diagnostics,
|
||||
state::{report_diagnostics, Source, Treehouse},
|
||||
vfs::{self, Dir, VPath},
|
||||
vfs::{self, Content, Dir, VPath},
|
||||
};
|
||||
|
||||
use super::WcArgs;
|
||||
|
@ -43,9 +43,8 @@ pub fn wc_cli(content_dir: &dyn Dir, mut wc_args: WcArgs) -> anyhow::Result<()>
|
|||
let mut total = 0;
|
||||
|
||||
for path in &wc_args.paths {
|
||||
if let Some(content) = content_dir
|
||||
.content(path)
|
||||
.and_then(|b| String::from_utf8(b).ok())
|
||||
if let Some(content) =
|
||||
vfs::query::<Content>(content_dir, path).and_then(|b| b.string().ok())
|
||||
{
|
||||
let file_id = treehouse.add_file(path.clone(), Source::Other(content.clone()));
|
||||
match parse_tree_with_diagnostics(file_id, &content) {
|
||||
|
|
|
@ -14,7 +14,7 @@ use crate::{
|
|||
Syntax,
|
||||
},
|
||||
import_map::ImportRoot,
|
||||
vfs::{self, Dir, DynDir, ImageSize, VPath, VPathBuf},
|
||||
vfs::{self, Content, Dir, DynDir, ImageSize, VPath, VPathBuf},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
@ -167,7 +167,9 @@ impl Config {
|
|||
}
|
||||
|
||||
pub fn pic_size(&self, pics_dir: &dyn Dir, id: &str) -> Option<ImageSize> {
|
||||
self.pics.get(id).and_then(|path| pics_dir.image_size(path))
|
||||
self.pics
|
||||
.get(id)
|
||||
.and_then(|path| vfs::query::<ImageSize>(pics_dir, path))
|
||||
}
|
||||
|
||||
/// Loads all syntax definition files.
|
||||
|
@ -188,12 +190,9 @@ impl Config {
|
|||
.file_stem()
|
||||
.expect("syntax file name should have a stem due to the .json extension");
|
||||
|
||||
let result: Result<Syntax, _> = dir
|
||||
.content(path)
|
||||
let result: Result<Syntax, _> = vfs::query::<Content>(&dir, path)
|
||||
.ok_or_else(|| anyhow!("syntax .json is not a file"))
|
||||
.and_then(|b| {
|
||||
String::from_utf8(b).context("syntax .json contains invalid UTF-8")
|
||||
})
|
||||
.and_then(|b| b.string().context("syntax .json contains invalid UTF-8"))
|
||||
.and_then(|s| {
|
||||
let _span = info_span!("Config::load_syntaxes::parse").entered();
|
||||
serde_json::from_str(&s).context("could not deserialize syntax file")
|
||||
|
|
|
@ -19,8 +19,8 @@ use crate::{
|
|||
fun::seasons::Season,
|
||||
sources::Sources,
|
||||
vfs::{
|
||||
self, Cd, ContentCache, Dir, DirEntry, DynDir, HtmlCanonicalize, MemDir, Overlay, ToDynDir,
|
||||
VPath, VPathBuf,
|
||||
self, Cd, Content, ContentCache, Dir, DynDir, Entries, HtmlCanonicalize, MemDir, Overlay,
|
||||
ToDynDir, VPath, VPathBuf,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -46,6 +46,36 @@ impl<'a> BaseTemplateData<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
handlebars_helper!(cat: |a: String, b: String| a + &b);
|
||||
|
||||
handlebars.register_helper("cat", Box::new(cat));
|
||||
handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone())));
|
||||
handlebars.register_helper(
|
||||
"include_static",
|
||||
Box::new(IncludeStaticHelper::new(static_)),
|
||||
);
|
||||
|
||||
handlebars
|
||||
}
|
||||
|
||||
#[instrument(skip(handlebars))]
|
||||
fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) {
|
||||
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
|
||||
if path.extension() == Some("hbs") {
|
||||
if let Some(content) = vfs::query::<Content>(dir, path).and_then(|c| c.string().ok()) {
|
||||
let _span = info_span!("register_template", ?path).entered();
|
||||
if let Err(err) = handlebars.register_template_string(path.as_str(), content) {
|
||||
error!("in template: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
}
|
||||
|
||||
struct TreehouseDir {
|
||||
dirs: Arc<Dirs>,
|
||||
sources: Arc<Sources>,
|
||||
|
@ -67,41 +97,9 @@ impl TreehouseDir {
|
|||
dir_index,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_handlebars(site: &str, static_: DynDir) -> Handlebars<'static> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
|
||||
handlebars_helper!(cat: |a: String, b: String| a + &b);
|
||||
|
||||
handlebars.register_helper("cat", Box::new(cat));
|
||||
handlebars.register_helper("asset", Box::new(DirHelper::new(site, static_.clone())));
|
||||
handlebars.register_helper(
|
||||
"include_static",
|
||||
Box::new(IncludeStaticHelper::new(static_)),
|
||||
);
|
||||
|
||||
handlebars
|
||||
}
|
||||
|
||||
#[instrument(skip(handlebars))]
|
||||
fn load_templates(handlebars: &mut Handlebars, dir: &dyn Dir) {
|
||||
vfs::walk_dir_rec(dir, VPath::ROOT, &mut |path| {
|
||||
if path.extension() == Some("hbs") {
|
||||
if let Some(content) = dir.content(path).and_then(|b| String::from_utf8(b).ok()) {
|
||||
let _span = info_span!("register_template", ?path).entered();
|
||||
if let Err(err) = handlebars.register_template_string(path.as_str(), content) {
|
||||
error!("in template: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
});
|
||||
}
|
||||
|
||||
impl Dir for TreehouseDir {
|
||||
#[instrument("TreehouseDir::dir", skip(self))]
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
|
||||
// NOTE: This does not include simple templates, because that's not really needed right now.
|
||||
|
||||
let mut index = &self.dir_index;
|
||||
|
@ -118,14 +116,12 @@ impl Dir for TreehouseDir {
|
|||
index
|
||||
.children
|
||||
.values()
|
||||
.map(|child| DirEntry {
|
||||
path: child.full_path.clone(),
|
||||
})
|
||||
.map(|child| child.full_path.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[instrument("TreehouseDir::content", skip(self))]
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
fn content(&self, path: &VPath) -> Option<Content> {
|
||||
let path = if path.is_root() {
|
||||
VPath::new_const("index")
|
||||
} else {
|
||||
|
@ -137,28 +133,34 @@ impl Dir for TreehouseDir {
|
|||
.files_by_tree_path
|
||||
.get(path)
|
||||
.map(|&file_id| {
|
||||
tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id).into()
|
||||
Content::new(
|
||||
tree::generate_or_error(&self.sources, &self.dirs, &self.handlebars, file_id)
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
.or_else(|| {
|
||||
if path.file_name().is_some_and(|s| !s.starts_with('_')) {
|
||||
let template_name = path.with_extension("hbs");
|
||||
if self.handlebars.has_template(template_name.as_str()) {
|
||||
return Some(
|
||||
return Some(Content::new(
|
||||
simple_template::generate_or_error(
|
||||
&self.sources,
|
||||
&self.handlebars,
|
||||
template_name.as_str(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn content_version(&self, _path: &VPath) -> Option<String> {
|
||||
None
|
||||
impl Dir for TreehouseDir {
|
||||
fn query(&self, path: &VPath, query: &mut vfs::Query) {
|
||||
query.provide(|| Entries(self.dir(path)));
|
||||
query.try_provide(|| self.content(path));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ use crate::{
|
|||
sources::Sources,
|
||||
state::FileId,
|
||||
tree::SemaBranchId,
|
||||
vfs::{Dir, DirEntry, VPath, VPathBuf},
|
||||
vfs::{self, Content, Dir, Entries, VPath, VPathBuf},
|
||||
};
|
||||
|
||||
use super::BaseTemplateData;
|
||||
|
@ -36,25 +36,21 @@ impl FeedDir {
|
|||
handlebars,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dir for FeedDir {
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
fn entries(&self, path: &VPath) -> Vec<VPathBuf> {
|
||||
if path == VPath::ROOT {
|
||||
self.sources
|
||||
.treehouse
|
||||
.feeds_by_name
|
||||
.keys()
|
||||
.map(|name| DirEntry {
|
||||
path: VPathBuf::new(format!("{name}.atom")),
|
||||
})
|
||||
.map(|name| VPathBuf::new(format!("{name}.atom")))
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
fn content(&self, path: &VPath) -> Option<Content> {
|
||||
info!("{path}");
|
||||
if path.extension() == Some("atom") {
|
||||
let feed_name = path.with_extension("").to_string();
|
||||
|
@ -63,15 +59,21 @@ impl Dir for FeedDir {
|
|||
.feeds_by_name
|
||||
.get(&feed_name)
|
||||
.map(|file_id| {
|
||||
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id).into()
|
||||
Content::new(
|
||||
generate_or_error(&self.sources, &self.dirs, &self.handlebars, *file_id)
|
||||
.into(),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn content_version(&self, _path: &VPath) -> Option<String> {
|
||||
None
|
||||
impl Dir for FeedDir {
|
||||
fn query(&self, path: &VPath, query: &mut vfs::Query) {
|
||||
query.provide(|| Entries(self.entries(path)));
|
||||
query.try_provide(|| self.content(path));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use handlebars::{Context, Handlebars, Helper, HelperDef, RenderContext, RenderError, ScopedJson};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::vfs::{DynDir, VPath};
|
||||
use crate::vfs::{self, Content, DynDir, VPath};
|
||||
|
||||
pub struct IncludeStaticHelper {
|
||||
dir: DynDir,
|
||||
|
@ -23,13 +23,11 @@ impl HelperDef for IncludeStaticHelper {
|
|||
) -> Result<ScopedJson<'reg, 'rc>, RenderError> {
|
||||
if let Some(path) = h.param(0).and_then(|v| v.value().as_str()) {
|
||||
let vpath = VPath::try_new(path).map_err(|e| RenderError::new(e.to_string()))?;
|
||||
let url = String::from_utf8(
|
||||
self.dir
|
||||
.content(vpath)
|
||||
.ok_or_else(|| RenderError::new("file does not exist"))?,
|
||||
)
|
||||
.map_err(|_| RenderError::new("included file does not contain UTF-8 text"))?;
|
||||
Ok(ScopedJson::Derived(Value::String(url)))
|
||||
let content = vfs::query::<Content>(&self.dir, vpath)
|
||||
.ok_or_else(|| RenderError::new("file does not exist"))?
|
||||
.string()
|
||||
.map_err(|_| RenderError::new("included file does not contain UTF-8 text"))?;
|
||||
Ok(ScopedJson::Derived(Value::String(content)))
|
||||
} else {
|
||||
Err(RenderError::new("missing path string"))
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ use crate::dirs::Dirs;
|
|||
use crate::state::FileId;
|
||||
use crate::state::Treehouse;
|
||||
use crate::vfs;
|
||||
use crate::vfs::ImageSize;
|
||||
|
||||
use super::highlight::highlight;
|
||||
|
||||
|
@ -584,7 +585,9 @@ impl<'a> Writer<'a> {
|
|||
write_attr(&url, out);
|
||||
out.push('"');
|
||||
|
||||
if let Some(image_size) = self.renderer.dirs.emoji.image_size(vpath) {
|
||||
if let Some(image_size) =
|
||||
vfs::query::<ImageSize>(&self.renderer.dirs.emoji, vpath)
|
||||
{
|
||||
write!(
|
||||
out,
|
||||
r#" width="{}" height="{}""#,
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
parse::parse_tree_with_diagnostics,
|
||||
state::{report_diagnostics, Source, Treehouse},
|
||||
tree::SemaRoots,
|
||||
vfs::{self, Cd, VPath, VPathBuf},
|
||||
vfs::{self, Cd, Content, VPath, VPathBuf},
|
||||
};
|
||||
|
||||
pub struct Sources {
|
||||
|
@ -27,10 +27,8 @@ impl Sources {
|
|||
let config = {
|
||||
let _span = info_span!("load_config").entered();
|
||||
let mut config: Config = toml_edit::de::from_str(
|
||||
&dirs
|
||||
.root
|
||||
.content(VPath::new("treehouse.toml"))
|
||||
.map(String::from_utf8)
|
||||
&vfs::query::<Content>(&dirs.root, VPath::new_const("treehouse.toml"))
|
||||
.map(Content::string)
|
||||
.ok_or_else(|| anyhow!("config file does not exist"))??,
|
||||
)
|
||||
.context("failed to deserialize config")?;
|
||||
|
@ -88,9 +86,8 @@ fn load_trees(config: &Config, dirs: &Dirs) -> anyhow::Result<Treehouse> {
|
|||
.into_par_iter()
|
||||
.zip(&file_ids)
|
||||
.flat_map(|(path, &file_id)| {
|
||||
dirs.content
|
||||
.content(&path)
|
||||
.and_then(|b| String::from_utf8(b).ok())
|
||||
vfs::query::<Content>(&dirs.content, &path)
|
||||
.and_then(|c| c.string().ok())
|
||||
.map(|input| {
|
||||
let parse_result = parse_tree_with_diagnostics(file_id, &input);
|
||||
(path, file_id, input, parse_result)
|
||||
|
|
|
@ -12,7 +12,7 @@ use crate::{
|
|||
dirs::Dirs,
|
||||
html::EscapeHtml,
|
||||
state::Treehouse,
|
||||
vfs::{Dir, VPath},
|
||||
vfs::{self, Content, VPath},
|
||||
};
|
||||
|
||||
struct Lexer<'a> {
|
||||
|
@ -206,8 +206,8 @@ impl Renderer<'_> {
|
|||
"pic" => Ok(config.pic_url(&*dirs.pic, arguments)),
|
||||
"include_static" => VPath::try_new(arguments)
|
||||
.ok()
|
||||
.and_then(|vpath| dirs.static_.content(vpath))
|
||||
.and_then(|content| String::from_utf8(content).ok())
|
||||
.and_then(|vpath| vfs::query::<Content>(&dirs.static_, vpath))
|
||||
.and_then(|c| c.string().ok())
|
||||
.ok_or(InvalidTemplate),
|
||||
_ => Err(InvalidTemplate),
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@
|
|||
//!
|
||||
//! In-memory directories can be composed using the following primitives:
|
||||
//!
|
||||
//! - [`EmptyEntry`] - has no metadata whatsoever.
|
||||
//! - [`BufferedFile`] - root path content is the provided byte vector.
|
||||
//! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside.
|
||||
//!
|
||||
|
@ -44,8 +43,10 @@
|
|||
//! [`VPath`] also has an owned version, [`VPathBuf`].
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
fmt::{self, Debug},
|
||||
ops::{ControlFlow, Deref},
|
||||
string::FromUtf8Error,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
|
@ -55,7 +56,6 @@ mod cd;
|
|||
mod content_cache;
|
||||
mod content_version_cache;
|
||||
mod edit;
|
||||
mod empty;
|
||||
mod file;
|
||||
mod html_canonicalize;
|
||||
mod image_size_cache;
|
||||
|
@ -69,7 +69,6 @@ pub use cd::*;
|
|||
pub use content_cache::*;
|
||||
pub use content_version_cache::*;
|
||||
pub use edit::*;
|
||||
pub use empty::*;
|
||||
pub use file::*;
|
||||
pub use html_canonicalize::*;
|
||||
pub use image_size_cache::*;
|
||||
|
@ -78,50 +77,94 @@ pub use overlay::*;
|
|||
pub use path::*;
|
||||
pub use physical::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct DirEntry {
|
||||
pub path: VPathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ImageSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
pub trait Dir: Debug {
|
||||
/// List all entries under the provided path.
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry>;
|
||||
fn query(&self, path: &VPath, query: &mut Query);
|
||||
}
|
||||
|
||||
/// Return the byte content of the entry at the given path.
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>>;
|
||||
pub trait Fork {}
|
||||
|
||||
/// Get a string signifying the current version of the provided path's content.
|
||||
/// If the content changes, the version must also change.
|
||||
///
|
||||
/// Returns None if there is no content or no version string is available.
|
||||
fn content_version(&self, path: &VPath) -> Option<String>;
|
||||
pub fn query<'a, T>(dir: &'a (impl Dir + ?Sized), path: &VPath) -> Option<T>
|
||||
where
|
||||
T: 'static + Fork,
|
||||
{
|
||||
let mut slot = TaggedOption::<'a, tags::Value<T>>(None);
|
||||
dir.query(path, Query::new(&mut slot));
|
||||
slot.0
|
||||
}
|
||||
|
||||
/// Returns the size of the image at the given path, or `None` if the entry is not an image
|
||||
/// (or its size cannot be known.)
|
||||
fn image_size(&self, _path: &VPath) -> Option<ImageSize> {
|
||||
None
|
||||
#[repr(transparent)]
|
||||
pub struct Query<'a> {
|
||||
erased: dyn Erased<'a> + 'a,
|
||||
}
|
||||
|
||||
impl<'a> Query<'a> {
|
||||
fn new<'b>(erased: &'b mut (dyn Erased<'a> + 'a)) -> &'b mut Query<'a> {
|
||||
unsafe { &mut *(erased as *mut dyn Erased<'a> as *mut Query<'a>) }
|
||||
}
|
||||
|
||||
/// Returns a path relative to `config.site` indicating where the file will be available
|
||||
/// once served.
|
||||
///
|
||||
/// May return `None` if the file is not served.
|
||||
fn anchor(&self, _path: &VPath) -> Option<VPathBuf> {
|
||||
None
|
||||
pub fn provide<T>(&mut self, f: impl FnOnce() -> T)
|
||||
where
|
||||
T: 'static + Fork,
|
||||
{
|
||||
if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
|
||||
result.0 = Some(f());
|
||||
}
|
||||
}
|
||||
|
||||
/// If a file can be written persistently, returns an [`EditPath`] representing the file in
|
||||
/// persistent storage.
|
||||
///
|
||||
/// An edit path can then be made into an [`Edit`].
|
||||
fn edit_path(&self, _path: &VPath) -> Option<EditPath> {
|
||||
None
|
||||
pub fn try_provide<T>(&mut self, f: impl FnOnce() -> Option<T>)
|
||||
where
|
||||
T: 'static + Fork,
|
||||
{
|
||||
if let Some(result @ TaggedOption(None)) = self.erased.downcast_mut::<tags::Value<T>>() {
|
||||
result.0 = f();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod tags {
|
||||
use std::marker::PhantomData;
|
||||
|
||||
pub trait Type<'a>: Sized + 'static {
|
||||
type Reified: 'a;
|
||||
}
|
||||
|
||||
pub struct Value<T>(PhantomData<T>)
|
||||
where
|
||||
T: 'static;
|
||||
|
||||
impl<T> Type<'_> for Value<T>
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
type Reified = T;
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(transparent)]
|
||||
struct TaggedOption<'a, I: tags::Type<'a>>(Option<I::Reified>);
|
||||
|
||||
#[expect(clippy::missing_safety_doc)]
|
||||
unsafe trait Erased<'a>: 'a {
|
||||
fn tag_id(&self) -> TypeId;
|
||||
}
|
||||
|
||||
unsafe impl<'a, I: tags::Type<'a>> Erased<'a> for TaggedOption<'a, I> {
|
||||
fn tag_id(&self) -> TypeId {
|
||||
TypeId::of::<I>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> dyn Erased<'a> + 'a {
|
||||
fn downcast_mut<I>(&mut self) -> Option<&mut TaggedOption<'a, I>>
|
||||
where
|
||||
I: tags::Type<'a>,
|
||||
{
|
||||
if self.tag_id() == TypeId::of::<I>() {
|
||||
// SAFETY: Just checked whether we're pointing to an I.
|
||||
Some(unsafe { &mut *(self as *mut Self).cast::<TaggedOption<'a, I>>() })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,28 +172,8 @@ impl<T> Dir for &T
|
|||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
(**self).dir(path)
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
(**self).content(path)
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
(**self).content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
(**self).image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
(**self).anchor(path)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
(**self).edit_path(path)
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
(**self).query(path, query)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,28 +183,8 @@ pub struct DynDir {
|
|||
}
|
||||
|
||||
impl Dir for DynDir {
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.arc.dir(path)
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.arc.content(path)
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.arc.content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.arc.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.arc.anchor(path)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.arc.edit_path(path)
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
self.arc.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,21 +232,75 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// List of child entries under a directory.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Entries(pub Vec<VPathBuf>);
|
||||
|
||||
/// Byte content in an entry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Content {
|
||||
bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Abstract version of an entry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ContentVersion {
|
||||
pub string: String,
|
||||
}
|
||||
|
||||
/// Path relative to `config.site` indicating where the file will be available once served.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Anchor {
|
||||
pub path: VPathBuf,
|
||||
}
|
||||
|
||||
/// Size of image entries.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ImageSize {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
impl Content {
|
||||
pub fn new(bytes: Vec<u8>) -> Self {
|
||||
Self { bytes }
|
||||
}
|
||||
|
||||
pub fn bytes(self) -> Vec<u8> {
|
||||
self.bytes
|
||||
}
|
||||
|
||||
pub fn string(self) -> Result<String, FromUtf8Error> {
|
||||
String::from_utf8(self.bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Fork for Entries {}
|
||||
impl Fork for Content {}
|
||||
impl Fork for ContentVersion {}
|
||||
impl Fork for Anchor {}
|
||||
impl Fork for ImageSize {}
|
||||
impl Fork for EditPath {}
|
||||
|
||||
pub fn entries(dir: &dyn Dir, path: &VPath) -> Vec<VPathBuf> {
|
||||
query::<Entries>(dir, path).map(|e| e.0).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) {
|
||||
for entry in dir.dir(path) {
|
||||
match f(&entry.path) {
|
||||
for entry in entries(dir, path) {
|
||||
match f(&entry) {
|
||||
ControlFlow::Continue(_) => (),
|
||||
ControlFlow::Break(_) => return,
|
||||
}
|
||||
walk_dir_rec(dir, &entry.path, f);
|
||||
walk_dir_rec(dir, &entry, f);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option<String> {
|
||||
let anchor = dir.anchor(path)?;
|
||||
if let Some(version) = dir.content_version(path) {
|
||||
Some(format!("{}/{anchor}?v={version}", site))
|
||||
let anchor = query::<Anchor>(dir, path)?;
|
||||
if let Some(version) = query::<ContentVersion>(dir, path) {
|
||||
Some(format!("{}/{}?v={}", site, anchor.path, version.string))
|
||||
} else {
|
||||
Some(format!("{}/{anchor}", site))
|
||||
Some(format!("{}/{}", site, anchor.path))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{Anchor, Dir, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct Anchored<T> {
|
||||
inner: T,
|
||||
|
@ -17,28 +17,12 @@ impl<T> Dir for Anchored<T>
|
|||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.inner.dir(path)
|
||||
}
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.provide(|| Anchor {
|
||||
path: self.at.join(path),
|
||||
});
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.inner.content(path)
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.inner.content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.inner.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
Some(self.at.join(path))
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
self.inner.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use super::{Dir, DynDir, VPath};
|
||||
use super::{query, Content, DynDir, VPath};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AsyncDir {
|
||||
|
@ -10,13 +10,13 @@ impl AsyncDir {
|
|||
Self { inner }
|
||||
}
|
||||
|
||||
pub async fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
pub async fn content(&self, path: &VPath) -> Option<Content> {
|
||||
let this = self.clone();
|
||||
let path = path.to_owned();
|
||||
// NOTE: Performance impact of spawning a blocking task may be a bit high in case
|
||||
// we add caching.
|
||||
// Measure throughput here.
|
||||
tokio::task::spawn_blocking(move || this.inner.content(&path))
|
||||
tokio::task::spawn_blocking(move || query::<Content>(&this.inner, &path))
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{entries, Dir, Entries, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct Cd<T> {
|
||||
parent: T,
|
||||
|
@ -13,42 +13,34 @@ impl<T> Cd<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Cd<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
|
||||
entries(&self.parent, &self.path.join(path))
|
||||
.into_iter()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.strip_prefix(&self.path)
|
||||
.expect("all entries must be anchored within `self.path`")
|
||||
.to_owned()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dir for Cd<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.parent
|
||||
.dir(&self.path.join(path))
|
||||
.into_iter()
|
||||
.map(|entry| DirEntry {
|
||||
path: entry
|
||||
.path
|
||||
.strip_prefix(&self.path)
|
||||
.expect("all entries must be anchored within `self.path`")
|
||||
.to_owned(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
// The only query that meaningfully needs to return something else is `dir`, which must
|
||||
// be modified to strip prefixes off of the parent's returned paths.
|
||||
query.provide(|| Entries(self.dir(path)));
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.parent.content_version(&self.path.join(path))
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.parent.content(&self.path.join(path))
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.parent.image_size(&self.path.join(path))
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.parent.anchor(&self.path.join(path))
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.parent.edit_path(&self.path.join(path))
|
||||
// Other queries can run unmodified, only passing them the right path.
|
||||
self.parent.query(&self.path.join(path), query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,11 +7,11 @@ use dashmap::DashMap;
|
|||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
|
||||
use tracing::{info_span, instrument};
|
||||
|
||||
use super::{walk_dir_rec, Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{query, walk_dir_rec, Content, Dir, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct ContentCache<T> {
|
||||
inner: T,
|
||||
cache: DashMap<VPathBuf, Vec<u8>>,
|
||||
cache: DashMap<VPathBuf, Content>,
|
||||
}
|
||||
|
||||
impl<T> ContentCache<T> {
|
||||
|
@ -39,41 +39,32 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Dir for ContentCache<T>
|
||||
impl<T> ContentCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.inner.dir(path)
|
||||
}
|
||||
|
||||
#[instrument(name = "ContentCache::content", skip(self))]
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
fn content(&self, path: &VPath) -> Option<Content> {
|
||||
self.cache.get(path).map(|x| x.clone()).or_else(|| {
|
||||
let _span = info_span!("cache_miss").entered();
|
||||
|
||||
let content = self.inner.content(path);
|
||||
let content = query::<Content>(&self.inner, path);
|
||||
if let Some(content) = &content {
|
||||
self.cache.insert(path.to_owned(), content.clone());
|
||||
}
|
||||
content
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.inner.content_version(path)
|
||||
}
|
||||
impl<T> Dir for ContentCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.try_provide(|| self.content(path));
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.inner.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.inner.anchor(path)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
self.inner.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,11 +3,11 @@ use std::fmt::{self, Debug};
|
|||
use dashmap::DashMap;
|
||||
use tracing::{info_span, instrument};
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{query, Content, ContentVersion, Dir, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct Blake3ContentVersionCache<T> {
|
||||
inner: T,
|
||||
cache: DashMap<VPathBuf, String>,
|
||||
cache: DashMap<VPathBuf, ContentVersion>,
|
||||
}
|
||||
|
||||
impl<T> Blake3ContentVersionCache<T> {
|
||||
|
@ -19,26 +19,20 @@ impl<T> Blake3ContentVersionCache<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Dir for Blake3ContentVersionCache<T>
|
||||
impl<T> Blake3ContentVersionCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.inner.dir(path)
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.inner.content(path)
|
||||
}
|
||||
|
||||
#[instrument(name = "Blake3ContentVersionCache::content_version", skip(self))]
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
fn content_version(&self, path: &VPath) -> Option<ContentVersion> {
|
||||
self.cache.get(path).map(|x| x.clone()).or_else(|| {
|
||||
let _span = info_span!("cache_miss").entered();
|
||||
|
||||
let version = self.inner.content(path).map(|content| {
|
||||
let hash = blake3::hash(&content).to_hex();
|
||||
format!("b3-{}", &hash[0..8])
|
||||
let version = query::<Content>(&self.inner, path).map(|content| {
|
||||
let hash = blake3::hash(&content.bytes()).to_hex();
|
||||
ContentVersion {
|
||||
string: format!("b3-{}", &hash[0..8]),
|
||||
}
|
||||
});
|
||||
if let Some(version) = &version {
|
||||
self.cache.insert(path.to_owned(), version.clone());
|
||||
|
@ -46,17 +40,16 @@ where
|
|||
version
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.inner.image_size(path)
|
||||
}
|
||||
impl<T> Dir for Blake3ContentVersionCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.try_provide(|| self.content_version(path));
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.inner.anchor(path)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
self.inner.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
use super::{Dir, DirEntry, VPath};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EmptyEntry;
|
||||
|
||||
impl Dir for EmptyEntry {
|
||||
fn dir(&self, _path: &VPath) -> Vec<DirEntry> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn content_version(&self, _path: &VPath) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn content(&self, _path: &VPath) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use std::fmt;
|
||||
|
||||
use super::{DirEntry, Dir, VPath};
|
||||
use super::{Content, Dir, Query, VPath};
|
||||
|
||||
pub struct BufferedFile {
|
||||
pub content: Vec<u8>,
|
||||
|
@ -13,20 +13,9 @@ impl BufferedFile {
|
|||
}
|
||||
|
||||
impl Dir for BufferedFile {
|
||||
fn dir(&self, _path: &VPath) -> Vec<DirEntry> {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn content_version(&self, _path: &VPath) -> Option<String> {
|
||||
// TODO: StaticFile should _probably_ calculate a content_version.
|
||||
None
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
if path == VPath::ROOT {
|
||||
Some(self.content.clone())
|
||||
} else {
|
||||
None
|
||||
query.provide(|| Content::new(self.content.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use core::fmt;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{Dir, Query, VPath};
|
||||
|
||||
pub struct HtmlCanonicalize<T> {
|
||||
inner: T,
|
||||
|
@ -16,33 +16,13 @@ impl<T> Dir for HtmlCanonicalize<T>
|
|||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.inner.dir(path)
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
let mut path = path.to_owned();
|
||||
if path.extension() == Some("html") {
|
||||
path.set_extension("");
|
||||
}
|
||||
|
||||
self.inner.content(&path)
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.inner.content_version(path)
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.inner.image_size(path)
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.inner.anchor(path)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
self.inner.query(&path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use tracing::{info_span, instrument, warn};
|
|||
|
||||
use crate::config;
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{query, Content, Dir, ImageSize, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct ImageSizeCache<T> {
|
||||
inner: T,
|
||||
|
@ -28,8 +28,8 @@ where
|
|||
{
|
||||
fn compute_image_size(&self, path: &VPath) -> anyhow::Result<Option<ImageSize>> {
|
||||
if path.extension().is_some_and(config::is_image_file) {
|
||||
if let Some(content) = self.content(path) {
|
||||
let reader = image::ImageReader::new(Cursor::new(content))
|
||||
if let Some(content) = query::<Content>(&self.inner, path) {
|
||||
let reader = image::ImageReader::new(Cursor::new(content.bytes()))
|
||||
.with_guessed_format()
|
||||
.context("cannot guess image format")?;
|
||||
let (width, height) = reader.into_dimensions()?;
|
||||
|
@ -39,23 +39,6 @@ where
|
|||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dir for ImageSizeCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
self.inner.dir(path)
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.inner.content(path)
|
||||
}
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.inner.content_version(path)
|
||||
}
|
||||
|
||||
#[instrument("ImageSizeCache::image_size", skip(self))]
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
|
@ -73,13 +56,16 @@ where
|
|||
image_size
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.inner.anchor(path)
|
||||
}
|
||||
impl<T> Dir for ImageSizeCache<T>
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.try_provide(|| self.image_size(path));
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.inner.edit_path(path)
|
||||
self.inner.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{collections::HashMap, fmt};
|
||||
|
||||
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct MemDir {
|
||||
mount_points: HashMap<String, DynDir>,
|
||||
|
@ -55,6 +55,23 @@ impl MemDir {
|
|||
|
||||
Resolved::None
|
||||
}
|
||||
|
||||
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
|
||||
match self.resolve(path) {
|
||||
Resolved::Root => self.mount_points.keys().map(VPathBuf::new).collect(),
|
||||
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path,
|
||||
subpath,
|
||||
} => entries(fs, subpath)
|
||||
.into_iter()
|
||||
.map(|path| fs_path.join(&path))
|
||||
.collect(),
|
||||
|
||||
Resolved::None => vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MemDir {
|
||||
|
@ -64,82 +81,16 @@ impl Default for MemDir {
|
|||
}
|
||||
|
||||
impl Dir for MemDir {
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
match self.resolve(path) {
|
||||
Resolved::Root => self
|
||||
.mount_points
|
||||
.keys()
|
||||
.map(|name| DirEntry {
|
||||
path: VPathBuf::new(name),
|
||||
})
|
||||
.collect(),
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path,
|
||||
subpath,
|
||||
} => fs
|
||||
.dir(subpath)
|
||||
.into_iter()
|
||||
.map(|entry| DirEntry {
|
||||
path: fs_path.join(&entry.path),
|
||||
})
|
||||
.collect(),
|
||||
Resolved::None => vec![],
|
||||
}
|
||||
}
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.provide(|| Entries(self.dir(path)));
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
match self.resolve(path) {
|
||||
Resolved::Root | Resolved::None => (),
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path: _,
|
||||
subpath,
|
||||
} => fs.content_version(subpath),
|
||||
Resolved::Root | Resolved::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
match self.resolve(path) {
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path: _,
|
||||
subpath,
|
||||
} => fs.content(subpath),
|
||||
Resolved::Root | Resolved::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
match self.resolve(path) {
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path: _,
|
||||
subpath,
|
||||
} => fs.image_size(subpath),
|
||||
Resolved::Root | Resolved::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
match self.resolve(path) {
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path: _,
|
||||
subpath,
|
||||
} => fs.anchor(subpath),
|
||||
Resolved::Root | Resolved::None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
match self.resolve(path) {
|
||||
Resolved::MountPoint {
|
||||
fs,
|
||||
fs_path: _,
|
||||
subpath,
|
||||
} => fs.edit_path(subpath),
|
||||
Resolved::Root | Resolved::None => None,
|
||||
} => fs.query(subpath, query),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::fmt;
|
|||
|
||||
use tracing::instrument;
|
||||
|
||||
use super::{Dir, DirEntry, DynDir, EditPath, ImageSize, VPath, VPathBuf};
|
||||
use super::{entries, Dir, DynDir, Entries, Query, VPath, VPathBuf};
|
||||
|
||||
pub struct Overlay {
|
||||
base: DynDir,
|
||||
|
@ -13,44 +13,23 @@ impl Overlay {
|
|||
pub fn new(base: DynDir, overlay: DynDir) -> Self {
|
||||
Self { base, overlay }
|
||||
}
|
||||
}
|
||||
|
||||
impl Dir for Overlay {
|
||||
#[instrument("Overlay::dir", skip(self))]
|
||||
fn dir(&self, path: &VPath) -> Vec<DirEntry> {
|
||||
let mut dir = self.base.dir(path);
|
||||
dir.append(&mut self.overlay.dir(path));
|
||||
fn dir(&self, path: &VPath) -> Vec<VPathBuf> {
|
||||
let mut dir = entries(&self.base, path);
|
||||
dir.append(&mut entries(&self.overlay, path));
|
||||
dir.sort();
|
||||
dir.dedup();
|
||||
dir
|
||||
}
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
self.overlay
|
||||
.content(path)
|
||||
.or_else(|| self.base.content(path))
|
||||
}
|
||||
impl Dir for Overlay {
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.provide(|| Entries(self.dir(path)));
|
||||
|
||||
fn content_version(&self, path: &VPath) -> Option<String> {
|
||||
self.overlay
|
||||
.content_version(path)
|
||||
.or_else(|| self.base.content_version(path))
|
||||
}
|
||||
|
||||
fn image_size(&self, path: &VPath) -> Option<ImageSize> {
|
||||
self.overlay
|
||||
.image_size(path)
|
||||
.or_else(|| self.base.image_size(path))
|
||||
}
|
||||
|
||||
fn anchor(&self, path: &VPath) -> Option<VPathBuf> {
|
||||
self.overlay.anchor(path).or_else(|| self.base.anchor(path))
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
self.overlay
|
||||
.edit_path(path)
|
||||
.or_else(|| self.base.edit_path(path))
|
||||
self.overlay.query(path, query);
|
||||
self.base.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use tracing::{error, instrument};
|
||||
|
||||
use super::{Dir, DirEntry, EditPath, VPath, VPathBuf};
|
||||
use super::{Content, Dir, EditPath, Entries, Query, VPath, VPathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PhysicalDir {
|
||||
|
@ -13,11 +13,8 @@ impl PhysicalDir {
|
|||
pub fn new(root: PathBuf) -> Self {
|
||||
Self { root }
|
||||
}
|
||||
}
|
||||
|
||||
impl Dir for PhysicalDir {
|
||||
#[instrument("PhysicalDir::dir", skip(self))]
|
||||
fn dir(&self, vpath: &VPath) -> Vec<DirEntry> {
|
||||
fn entries(&self, vpath: &VPath) -> Vec<VPathBuf> {
|
||||
let physical = self.root.join(physical_path(vpath));
|
||||
if !physical.is_dir() {
|
||||
return vec![];
|
||||
|
@ -47,7 +44,7 @@ impl Dir for PhysicalDir {
|
|||
error!("{self:?} error with vpath for {path_str:?}: {err:?}");
|
||||
})
|
||||
.ok()?;
|
||||
Some(DirEntry { path: vpath_buf })
|
||||
Some(vpath_buf)
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
|
@ -60,21 +57,26 @@ impl Dir for PhysicalDir {
|
|||
}
|
||||
}
|
||||
|
||||
fn content_version(&self, _path: &VPath) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
#[instrument("PhysicalDir::content", skip(self))]
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
fn content(&self, path: &VPath) -> Option<Content> {
|
||||
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)
|
||||
}
|
||||
|
||||
fn edit_path(&self, path: &VPath) -> Option<EditPath> {
|
||||
Some(EditPath {
|
||||
fn edit_path(&self, path: &VPath) -> EditPath {
|
||||
EditPath {
|
||||
path: self.root.join(physical_path(path)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dir for PhysicalDir {
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
query.provide(|| Entries(self.entries(path)));
|
||||
query.try_provide(|| self.content(path));
|
||||
query.provide(|| self.edit_path(path));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
mod cd;
|
||||
mod empty;
|
||||
mod file;
|
||||
mod mount_points;
|
||||
mod physical;
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use treehouse::vfs::{BufferedFile, Cd, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf};
|
||||
use treehouse::vfs::{
|
||||
entries, query, BufferedFile, Cd, Content, MemDir, ToDynDir, VPath, VPathBuf,
|
||||
};
|
||||
|
||||
const HEWWO: &[u8] = b"hewwo :3";
|
||||
const FWOOFEE: &[u8] = b"fwoofee -w-";
|
||||
|
@ -27,20 +29,14 @@ fn dir1() {
|
|||
let outer = vfs();
|
||||
let inner = Cd::new(outer, VPathBuf::new("inner"));
|
||||
|
||||
let mut dir = inner.dir(VPath::ROOT);
|
||||
let mut dir = entries(&inner, VPath::ROOT);
|
||||
dir.sort();
|
||||
assert_eq!(
|
||||
dir,
|
||||
vec![
|
||||
DirEntry {
|
||||
path: VPathBuf::new("file1.txt"),
|
||||
},
|
||||
DirEntry {
|
||||
path: VPathBuf::new("file2.txt"),
|
||||
},
|
||||
DirEntry {
|
||||
path: VPathBuf::new("innermost"),
|
||||
}
|
||||
VPathBuf::new("file1.txt"),
|
||||
VPathBuf::new("file2.txt"),
|
||||
VPathBuf::new("innermost"),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
@ -50,25 +46,9 @@ fn dir2() {
|
|||
let outer = vfs();
|
||||
let innermost = Cd::new(&outer, VPathBuf::new("inner/innermost"));
|
||||
|
||||
let mut dir = innermost.dir(VPath::ROOT);
|
||||
let mut dir = entries(&innermost, VPath::ROOT);
|
||||
dir.sort();
|
||||
assert_eq!(
|
||||
dir,
|
||||
vec![DirEntry {
|
||||
path: VPathBuf::new("file3.txt"),
|
||||
},]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_version() {
|
||||
let outer = vfs();
|
||||
let inner = Cd::new(&outer, VPathBuf::new("inner"));
|
||||
|
||||
assert_eq!(
|
||||
inner.content_version(VPath::new("test1.txt")),
|
||||
outer.content_version(VPath::new("inner/test1.txt"))
|
||||
);
|
||||
assert_eq!(dir, vec![VPathBuf::new("file3.txt")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -77,7 +57,7 @@ fn content() {
|
|||
let inner = Cd::new(&outer, VPathBuf::new("inner"));
|
||||
|
||||
assert_eq!(
|
||||
inner.content(VPath::new("test1.txt")),
|
||||
outer.content(VPath::new("inner/test1.txt"))
|
||||
query::<Content>(&inner, VPath::new("test1.txt")).map(Content::bytes),
|
||||
query::<Content>(&outer, VPath::new("inner/test1.txt")).map(Content::bytes)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
use treehouse::vfs::{Dir, EmptyEntry, VPath};
|
||||
|
||||
#[test]
|
||||
fn dir() {
|
||||
assert!(EmptyEntry.dir(VPath::ROOT).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_version() {
|
||||
assert!(EmptyEntry.content_version(VPath::ROOT).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content() {
|
||||
assert!(EmptyEntry.content(VPath::ROOT).is_none());
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
use treehouse::vfs::{BufferedFile, Dir, VPath};
|
||||
use treehouse::vfs::{entries, query, BufferedFile, Content, VPath};
|
||||
|
||||
fn vfs() -> BufferedFile {
|
||||
BufferedFile::new(b"hewwo :3".to_vec())
|
||||
|
@ -7,23 +7,16 @@ fn vfs() -> BufferedFile {
|
|||
#[test]
|
||||
fn dir() {
|
||||
let vfs = vfs();
|
||||
assert!(vfs.dir(VPath::ROOT).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_version() {
|
||||
let vfs = vfs();
|
||||
assert!(
|
||||
vfs.content_version(VPath::ROOT).is_none(),
|
||||
"content_version is not implemented for BufferedFile for now"
|
||||
);
|
||||
assert!(entries(&vfs, VPath::ROOT).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content() {
|
||||
let vfs = vfs();
|
||||
assert_eq!(
|
||||
vfs.content(VPath::ROOT).as_deref(),
|
||||
query::<Content>(&vfs, VPath::ROOT)
|
||||
.map(|c| c.bytes())
|
||||
.as_deref(),
|
||||
Some(b"hewwo :3".as_slice()),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
use treehouse::vfs::{BufferedFile, Dir, DirEntry, MemDir, ToDynDir, VPath, VPathBuf};
|
||||
use treehouse::vfs::{
|
||||
entries, query, BufferedFile, Content, Dir, MemDir, ToDynDir, VPath, VPathBuf,
|
||||
};
|
||||
|
||||
const HEWWO: &[u8] = b"hewwo :3";
|
||||
const FWOOFEE: &[u8] = b"fwoofee -w-";
|
||||
|
@ -23,52 +25,22 @@ fn vfs() -> MemDir {
|
|||
fn dir() {
|
||||
let vfs = vfs();
|
||||
|
||||
let mut dir = vfs.dir(VPath::new(""));
|
||||
let mut dir = entries(&vfs, VPath::ROOT);
|
||||
dir.sort();
|
||||
assert_eq!(
|
||||
dir,
|
||||
vec![
|
||||
DirEntry {
|
||||
path: VPathBuf::new("file1.txt"),
|
||||
},
|
||||
DirEntry {
|
||||
path: VPathBuf::new("file2.txt"),
|
||||
},
|
||||
DirEntry {
|
||||
path: VPathBuf::new("inner"),
|
||||
}
|
||||
VPathBuf::new("file1.txt"),
|
||||
VPathBuf::new("file2.txt"),
|
||||
VPathBuf::new("inner"),
|
||||
]
|
||||
);
|
||||
|
||||
assert!(vfs.dir(VPath::new("file1.txt")).is_empty());
|
||||
assert!(vfs.dir(VPath::new("file2.txt")).is_empty());
|
||||
assert!(entries(&vfs, VPath::new("file1.txt")).is_empty());
|
||||
assert!(entries(&vfs, VPath::new("file2.txt")).is_empty());
|
||||
assert_eq!(
|
||||
vfs.dir(VPath::new("inner")),
|
||||
vec![DirEntry {
|
||||
path: VPathBuf::new("inner/file3.txt")
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_version() {
|
||||
let vfs = vfs();
|
||||
|
||||
let file1 = BufferedFile::new(HEWWO.to_vec());
|
||||
let file2 = BufferedFile::new(FWOOFEE.to_vec());
|
||||
let file3 = BufferedFile::new(BOOP.to_vec());
|
||||
|
||||
assert_eq!(
|
||||
vfs.content_version(VPath::new("file1.txt")),
|
||||
file1.content_version(VPath::ROOT)
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.content_version(VPath::new("file2.txt")),
|
||||
file2.content_version(VPath::ROOT)
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.content_version(VPath::new("inner/file3.txt")),
|
||||
file3.content_version(VPath::ROOT)
|
||||
entries(&vfs, VPath::new("inner")),
|
||||
vec![VPathBuf::new("inner/file3.txt")]
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -76,13 +48,22 @@ fn content_version() {
|
|||
fn content() {
|
||||
let vfs = vfs();
|
||||
|
||||
assert_eq!(vfs.content(VPath::new("file1.txt")).as_deref(), Some(HEWWO));
|
||||
assert_eq!(
|
||||
vfs.content(VPath::new("file2.txt")).as_deref(),
|
||||
query::<Content>(&vfs, VPath::new("file1.txt"))
|
||||
.map(Content::bytes)
|
||||
.as_deref(),
|
||||
Some(HEWWO)
|
||||
);
|
||||
assert_eq!(
|
||||
query::<Content>(&vfs, VPath::new("file2.txt"))
|
||||
.map(Content::bytes)
|
||||
.as_deref(),
|
||||
Some(FWOOFEE)
|
||||
);
|
||||
assert_eq!(
|
||||
vfs.content(VPath::new("inner/file3.txt")).as_deref(),
|
||||
query::<Content>(&vfs, VPath::new("inner/file3.txt"))
|
||||
.map(Content::bytes)
|
||||
.as_deref(),
|
||||
Some(BOOP)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::path::Path;
|
||||
|
||||
use treehouse::vfs::{DirEntry, PhysicalDir, Dir, VPath, VPathBuf};
|
||||
use treehouse::vfs::{entries, query, Content, PhysicalDir, VPath, VPathBuf};
|
||||
|
||||
fn vfs() -> PhysicalDir {
|
||||
let root = Path::new("tests/it/vfs_physical").to_path_buf();
|
||||
|
@ -10,28 +10,13 @@ fn vfs() -> PhysicalDir {
|
|||
#[test]
|
||||
fn dir() {
|
||||
let vfs = vfs();
|
||||
let dir = vfs.dir(VPath::ROOT);
|
||||
assert_eq!(
|
||||
&dir[..],
|
||||
&[DirEntry {
|
||||
path: VPathBuf::new("test.txt"),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content_version() {
|
||||
let vfs = vfs();
|
||||
let content_version = vfs.content_version(VPath::new("test.txt"));
|
||||
assert_eq!(
|
||||
content_version, None,
|
||||
"content_version remains unimplemented for now"
|
||||
);
|
||||
let dir = entries(&vfs, VPath::ROOT);
|
||||
assert_eq!(&dir[..], &[VPathBuf::new("test.txt")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn content() {
|
||||
let vfs = vfs();
|
||||
let content = vfs.content(VPath::new("test.txt"));
|
||||
let content = query::<Content>(&vfs, VPath::new("test.txt")).map(Content::bytes);
|
||||
assert_eq!(content.as_deref(), Some(b"hewwo :3\n".as_slice()));
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue