refactor: introduce virtual file system as a central router for source and target data
This commit is contained in:
		
							parent
							
								
									0b0c0a4de4
								
							
						
					
					
						commit
						c4912f4917
					
				
					 4 changed files with 239 additions and 0 deletions
				
			
		
							
								
								
									
										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…
	
	Add table
		Add a link
		
	
		Reference in a new issue