diff --git a/content/treehouse/vfs.tree b/content/treehouse/vfs.tree new file mode 100644 index 0000000..e4f9a31 --- /dev/null +++ b/content/treehouse/vfs.tree @@ -0,0 +1,17 @@ +%% title = "treehouse virtual file system design" + +- notes on the design; this is not an actual listing of the virtual file system + +- `content` - `GitDir(".", "content")` + + - `GitDir` is a special filesystem which makes all files have subpaths with commit data sourced from git. + their entries are ordered by how new/old a commit is + + - `inner/` - contains the file content and a revision info fork + + - `inner/latest` - same but for the latest revision, if applicable. + this may be the working tree + +- `template` - `PhysicalDir("template")` + +- `static` - `PhysicalDir("static")` diff --git a/crates/treehouse/src/main.rs b/crates/treehouse/src/main.rs index f38a216..75b3927 100644 --- a/crates/treehouse/src/main.rs +++ b/crates/treehouse/src/main.rs @@ -23,6 +23,7 @@ mod paths; mod state; mod static_urls; mod tree; +mod vfs; async fn fallible_main() -> anyhow::Result<()> { let args = ProgramArgs::parse(); diff --git a/crates/treehouse/src/vfs.rs b/crates/treehouse/src/vfs.rs new file mode 100644 index 0000000..ff4209b --- /dev/null +++ b/crates/treehouse/src/vfs.rs @@ -0,0 +1,139 @@ +use std::{borrow::Borrow, fmt, ops::Deref}; + +use anyhow::ensure; + +pub mod physical; + +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VPath { + path: str, +} + +impl VPath { + pub const SEPARATOR: char = '/'; + + pub fn try_new(s: &str) -> anyhow::Result<&Self> { + ensure!( + !s.ends_with(Self::SEPARATOR), + "path must not end with '{}'", + Self::SEPARATOR + ); + ensure!( + !s.starts_with(Self::SEPARATOR), + "paths are always absolute and must not start with '{}'", + Self::SEPARATOR + ); + + Ok(unsafe { Self::new_unchecked(s) }) + } + + pub fn new(s: &str) -> &Self { + Self::try_new(s).expect("invalid path") + } + + unsafe fn new_unchecked(s: &str) -> &Self { + std::mem::transmute::<_, &Self>(s) + } + + pub fn try_join(&self, sub: &str) -> anyhow::Result { + let mut buf = VPathBuf::from(self); + let sub = VPath::try_new(sub)?; + buf.path.push_str(&sub.path); + Ok(buf) + } + + pub fn join(&self, sub: &str) -> VPathBuf { + self.try_join(sub).expect("invalid subpath") + } + + pub fn strip_prefix(&self, prefix: &VPath) -> Option<&Self> { + self.path + .strip_prefix(&prefix.path) + .and_then(|p| p.strip_prefix('/')) + // SAFETY: If `self` starts with `prefix`, `p` will end up not being prefixed by `self` + // nor a leading slash. + .map(|p| unsafe { VPath::new_unchecked(p) }) + } + + pub fn str(&self) -> &str { + &self.path + } +} + +impl ToOwned for VPath { + type Owned = VPathBuf; + + fn to_owned(&self) -> Self::Owned { + VPathBuf::from(self) + } +} + +impl fmt::Debug for VPath { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.path) + } +} + +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct VPathBuf { + path: String, +} + +impl VPathBuf { + pub fn new(path: impl Into) -> anyhow::Result { + let path = path.into(); + match VPath::try_new(&path) { + Ok(_) => Ok(Self { path }), + Err(e) => Err(e), + } + } + + unsafe fn new_unchecked(path: String) -> Self { + Self { path } + } +} + +impl Deref for VPathBuf { + type Target = VPath; + + fn deref(&self) -> &Self::Target { + unsafe { VPath::new_unchecked(&self.path) } + } +} + +impl fmt::Debug for VPathBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.path) + } +} + +impl From<&VPath> for VPathBuf { + fn from(value: &VPath) -> Self { + unsafe { Self::new_unchecked(value.path.to_owned()) } + } +} + +impl Borrow for VPathBuf { + fn borrow(&self) -> &VPath { + self + } +} + +#[derive(Debug, Clone)] +pub struct DirEntry { + pub path: VPathBuf, +} + +pub trait ReadFilesystem { + /// List all files under the provided path. + fn dir(&self, path: &VPath) -> Vec; + + /// 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; + + /// Return the byte content of the entry at the given path. + fn content(&self, path: &VPath) -> Option>; +} diff --git a/crates/treehouse/src/vfs/physical.rs b/crates/treehouse/src/vfs/physical.rs new file mode 100644 index 0000000..b554d62 --- /dev/null +++ b/crates/treehouse/src/vfs/physical.rs @@ -0,0 +1,82 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use anyhow::Context; +use log::{error, warn}; +use walkdir::WalkDir; + +use super::{DirEntry, ReadFilesystem, VPath, VPathBuf}; + +#[derive(Debug, Clone)] +pub struct PhysicalDir { + root: PathBuf, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Entry { + Dir, + File, +} + +impl PhysicalDir { + pub fn new(root: PathBuf) -> Self { + Self { root } + } +} + +impl ReadFilesystem for PhysicalDir { + fn dir(&self, vpath: &VPath) -> Vec { + let physical = self.root.join(physical_path(vpath)); + match std::fs::read_dir(physical) { + Ok(read_dir) => read_dir + .filter_map(|entry| { + entry + .inspect_err(|err| { + error!( + "PhysicalDir {:?} error while reading entries in vpath {vpath:?}: {err:?}", + self.root + ) + }) + .ok() + .and_then(|entry| { + let path = entry.path(); + let path_str = match path.strip_prefix(&self.root).unwrap_or(&path).to_str() { + Some(p) => p, + None => { + error!("PhysicalDir {:?} entry {path:?} has invalid UTF-8 (while reading vpath {vpath:?})", self.root); + return None; + }, + }; + let vpath_buf = VPathBuf::new(path_str.replace('\\', "/")) + .inspect_err(|err| { + error!("PhysicalDir {:?} error with vpath for {path_str:?}: {err:?}", self.root); + }) + .ok()?; + Some(DirEntry { path: vpath_buf }) + }) + }) + .collect(), + Err(err) => { + error!( + "PhysicalDir {:?} cannot read vpath {vpath:?}: {err:?}", + self.root + ); + vec![] + } + } + } + + fn content_version(&self, path: &VPath) -> Option { + None + } + + fn content(&self, path: &VPath) -> Option> { + None + } +} + +fn physical_path(path: &VPath) -> &Path { + Path::new(path.str()) +}