refactor: introduce virtual file system as a central router for source and target data
This commit is contained in:
parent
0b0c0a4de4
commit
c4912f4917
17
content/treehouse/vfs.tree
Normal file
17
content/treehouse/vfs.tree
Normal file
|
@ -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/<commit>` - 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")`
|
|
@ -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();
|
||||
|
|
139
crates/treehouse/src/vfs.rs
Normal file
139
crates/treehouse/src/vfs.rs
Normal file
|
@ -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<VPathBuf> {
|
||||
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<String>) -> anyhow::Result<Self> {
|
||||
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<VPath> 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<DirEntry>;
|
||||
|
||||
/// 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>;
|
||||
|
||||
/// Return the byte content of the entry at the given path.
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>>;
|
||||
}
|
82
crates/treehouse/src/vfs/physical.rs
Normal file
82
crates/treehouse/src/vfs/physical.rs
Normal file
|
@ -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<DirEntry> {
|
||||
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<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn content(&self, path: &VPath) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn physical_path(path: &VPath) -> &Path {
|
||||
Path::new(path.str())
|
||||
}
|
Loading…
Reference in a new issue