remove treehouse-format crate and collapse everything into src
This commit is contained in:
parent
ca127a9411
commit
b792688776
66 changed files with 145 additions and 112 deletions
315
src/vfs.rs
Normal file
315
src/vfs.rs
Normal file
|
@ -0,0 +1,315 @@
|
|||
//! The treehouse virtual file system.
|
||||
//!
|
||||
//! Unlike traditional file systems, there is no separation between directories and files.
|
||||
//! Instead, our file system is based on _entries_, which may have specific, optional, well-typed
|
||||
//! metadata attached to them.
|
||||
//! A directory is formed by returning a list of paths from [`dir`][Dir::dir], and a file is
|
||||
//! formed by returning `Some` from [`content`][Dir::content].
|
||||
//!
|
||||
//! This makes using the file system simpler, as you do not have to differentiate between different
|
||||
//! entry kinds. All paths act as if they _could_ return byte content, and all paths act as if they
|
||||
//! _could_ have children.
|
||||
//!
|
||||
//! # Composability
|
||||
//!
|
||||
//! [`Dir`]s are composable. The [`Dir`] itself starts off with the root path ([`VPath::ROOT`]),
|
||||
//! which may contain further [`dir`][Dir::dir] entries, or content by itself.
|
||||
//! This makes it possible to nest a [`Dir`] under another [`Dir`].
|
||||
//!
|
||||
//! Additionally, there's also the inverse operation, [`Cd`] (named after the `cd`
|
||||
//! _change directory_ shell command), which returns a [`Dir`] viewing a subpath within another
|
||||
//! [`Dir`].
|
||||
//!
|
||||
//! # Building directories
|
||||
//!
|
||||
//! In-memory directories can be composed using the following primitives:
|
||||
//!
|
||||
//! - [`BufferedFile`] - root path content is the provided byte vector.
|
||||
//! - [`MemDir`] - a [`Dir`] containing a single level of other [`Dir`]s inside.
|
||||
//!
|
||||
//! Additionally, for interfacing with the OS file system, [`PhysicalDir`] is available,
|
||||
//! representing a directory stored on the disk.
|
||||
//!
|
||||
//! # Virtual paths
|
||||
//!
|
||||
//! Entries within directories are referenced using [`VPath`]s (**v**irtual **path**s).
|
||||
//! A virtual path is composed out of any amount of `/`-separated components.
|
||||
//!
|
||||
//! There are no special directories like `.` and `..` (those are just normal entries, though using
|
||||
//! them is discouraged). [`VPath`]s are always relative to the root of the [`Dir`] you're querying.
|
||||
//!
|
||||
//! A leading or trailing slash is not allowed, because they would have no meaning.
|
||||
//!
|
||||
//! [`VPath`] also has an owned version, [`VPathBuf`].
|
||||
|
||||
use std::{
|
||||
any::TypeId,
|
||||
fmt::{self, Debug},
|
||||
ops::{ControlFlow, Deref},
|
||||
string::FromUtf8Error,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
mod anchored;
|
||||
pub mod asynch;
|
||||
mod cd;
|
||||
mod content_cache;
|
||||
mod content_version_cache;
|
||||
mod edit;
|
||||
mod file;
|
||||
mod html_canonicalize;
|
||||
mod image_size_cache;
|
||||
mod mem_dir;
|
||||
mod overlay;
|
||||
mod path;
|
||||
mod physical;
|
||||
|
||||
pub use anchored::*;
|
||||
pub use cd::*;
|
||||
pub use content_cache::*;
|
||||
pub use content_version_cache::*;
|
||||
pub use edit::*;
|
||||
pub use file::*;
|
||||
pub use html_canonicalize::*;
|
||||
pub use image_size_cache::*;
|
||||
pub use mem_dir::*;
|
||||
pub use overlay::*;
|
||||
pub use path::*;
|
||||
pub use physical::*;
|
||||
|
||||
pub trait Dir: Debug {
|
||||
fn query(&self, path: &VPath, query: &mut Query);
|
||||
}
|
||||
|
||||
pub trait Fork {}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
#[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>) }
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Dir for &T
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
(**self).query(path, query)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DynDir {
|
||||
arc: Arc<dyn Dir + Send + Sync>,
|
||||
}
|
||||
|
||||
impl Dir for DynDir {
|
||||
fn query(&self, path: &VPath, query: &mut Query) {
|
||||
self.arc.query(path, query);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for DynDir {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Debug::fmt(&*self.arc, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DynDir {
|
||||
type Target = dyn Dir + Send + Sync;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.arc
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ToDynDir {
|
||||
fn to_dyn(self) -> DynDir;
|
||||
}
|
||||
|
||||
impl<T> ToDynDir for T
|
||||
where
|
||||
T: Dir + Send + Sync + 'static,
|
||||
{
|
||||
fn to_dyn(self) -> DynDir {
|
||||
DynDir {
|
||||
arc: Arc::new(self),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AnchoredAtExt {
|
||||
fn anchored_at(self, at: VPathBuf) -> Anchored<Self>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
impl<T> AnchoredAtExt for T
|
||||
where
|
||||
T: Dir,
|
||||
{
|
||||
fn anchored_at(self, at: VPathBuf) -> Anchored<Self> {
|
||||
Anchored::new(self, at)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
/// Media type string. <https://en.wikipedia.org/wiki/Media_type>
|
||||
kind: String,
|
||||
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(kind: impl Into<String>, bytes: Vec<u8>) -> Self {
|
||||
Self {
|
||||
kind: kind.into(),
|
||||
bytes,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> &str {
|
||||
&self.kind
|
||||
}
|
||||
|
||||
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 entries(dir, path) {
|
||||
match f(&entry) {
|
||||
ControlFlow::Continue(_) => (),
|
||||
ControlFlow::Break(_) => return,
|
||||
}
|
||||
walk_dir_rec(dir, &entry, f);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option<String> {
|
||||
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!("{}/{}", site, anchor.path))
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue