2024-11-17 22:34:43 +01:00
|
|
|
//! 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`].
|
|
|
|
|
2024-11-16 18:33:41 +01:00
|
|
|
use std::{
|
2024-11-29 20:03:32 +01:00
|
|
|
any::TypeId,
|
2024-11-16 18:33:41 +01:00
|
|
|
fmt::{self, Debug},
|
|
|
|
ops::{ControlFlow, Deref},
|
2024-11-29 20:03:32 +01:00
|
|
|
string::FromUtf8Error,
|
2024-11-17 22:34:43 +01:00
|
|
|
sync::Arc,
|
2024-11-16 18:33:41 +01:00
|
|
|
};
|
2024-11-08 14:52:32 +01:00
|
|
|
|
2024-11-16 18:33:41 +01:00
|
|
|
mod anchored;
|
2024-11-17 22:34:43 +01:00
|
|
|
pub mod asynch;
|
2024-11-16 18:33:41 +01:00
|
|
|
mod cd;
|
2024-11-23 21:21:28 +01:00
|
|
|
mod content_cache;
|
2024-11-23 18:29:03 +01:00
|
|
|
mod content_version_cache;
|
2024-11-17 22:34:43 +01:00
|
|
|
mod edit;
|
2024-11-16 18:33:41 +01:00
|
|
|
mod file;
|
2024-11-27 18:46:10 +01:00
|
|
|
mod html_canonicalize;
|
2024-11-23 20:43:26 +01:00
|
|
|
mod image_size_cache;
|
2024-11-17 22:34:43 +01:00
|
|
|
mod mem_dir;
|
|
|
|
mod overlay;
|
|
|
|
mod path;
|
2024-11-16 18:33:41 +01:00
|
|
|
mod physical;
|
|
|
|
|
|
|
|
pub use anchored::*;
|
|
|
|
pub use cd::*;
|
2024-11-23 21:21:28 +01:00
|
|
|
pub use content_cache::*;
|
2024-11-23 18:29:03 +01:00
|
|
|
pub use content_version_cache::*;
|
2024-11-17 22:34:43 +01:00
|
|
|
pub use edit::*;
|
2024-11-16 18:33:41 +01:00
|
|
|
pub use file::*;
|
2024-11-27 18:46:10 +01:00
|
|
|
pub use html_canonicalize::*;
|
2024-11-23 20:43:26 +01:00
|
|
|
pub use image_size_cache::*;
|
2024-11-17 22:34:43 +01:00
|
|
|
pub use mem_dir::*;
|
|
|
|
pub use overlay::*;
|
|
|
|
pub use path::*;
|
2024-11-16 18:33:41 +01:00
|
|
|
pub use physical::*;
|
2024-11-08 14:52:32 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
pub trait Dir: Debug {
|
|
|
|
fn query(&self, path: &VPath, query: &mut Query);
|
2024-11-23 20:43:26 +01:00
|
|
|
}
|
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
pub trait Fork {}
|
2024-11-08 14:52:32 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
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
|
|
|
|
}
|
2024-11-08 14:52:32 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
#[repr(transparent)]
|
|
|
|
pub struct Query<'a> {
|
|
|
|
erased: dyn Erased<'a> + 'a,
|
|
|
|
}
|
2024-11-08 14:52:32 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
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>) }
|
2024-11-23 20:43:26 +01:00
|
|
|
}
|
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
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());
|
|
|
|
}
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
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();
|
|
|
|
}
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
2024-11-17 22:34:43 +01:00
|
|
|
}
|
2024-11-16 18:33:41 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
mod tags {
|
|
|
|
use std::marker::PhantomData;
|
2024-11-16 18:33:41 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
pub trait Type<'a>: Sized + 'static {
|
|
|
|
type Reified: 'a;
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
pub struct Value<T>(PhantomData<T>)
|
|
|
|
where
|
|
|
|
T: 'static;
|
|
|
|
|
|
|
|
impl<T> Type<'_> for Value<T>
|
|
|
|
where
|
|
|
|
T: 'static,
|
|
|
|
{
|
|
|
|
type Reified = T;
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
2024-11-29 20:03:32 +01:00
|
|
|
}
|
2024-11-16 18:33:41 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
#[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>()
|
2024-11-23 20:43:26 +01:00
|
|
|
}
|
2024-11-29 20:03:32 +01:00
|
|
|
}
|
2024-11-23 20:43:26 +01:00
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
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
|
|
|
|
}
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
2024-11-29 20:03:32 +01:00
|
|
|
}
|
2024-11-08 14:52:32 +01:00
|
|
|
|
2025-07-10 16:50:41 +02:00
|
|
|
impl Dir for () {
|
|
|
|
fn query(&self, _path: &VPath, _query: &mut Query) {
|
|
|
|
// Noop implementation.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
impl<T> Dir for &T
|
|
|
|
where
|
|
|
|
T: Dir,
|
|
|
|
{
|
|
|
|
fn query(&self, path: &VPath, query: &mut Query) {
|
|
|
|
(**self).query(path, query)
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct DynDir {
|
|
|
|
arc: Arc<dyn Dir + Send + Sync>,
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
impl Dir for DynDir {
|
2024-11-29 20:03:32 +01:00
|
|
|
fn query(&self, path: &VPath, query: &mut Query) {
|
|
|
|
self.arc.query(path, query);
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
impl fmt::Debug for DynDir {
|
2024-11-08 14:52:32 +01:00
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
2024-11-17 22:34:43 +01:00
|
|
|
fmt::Debug::fmt(&*self.arc, f)
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
impl Deref for DynDir {
|
|
|
|
type Target = dyn Dir + Send + Sync;
|
2024-11-16 18:33:41 +01:00
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
fn deref(&self) -> &Self::Target {
|
|
|
|
&*self.arc
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
pub trait ToDynDir {
|
|
|
|
fn to_dyn(self) -> DynDir;
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
impl<T> ToDynDir for T
|
|
|
|
where
|
|
|
|
T: Dir + Send + Sync + 'static,
|
|
|
|
{
|
|
|
|
fn to_dyn(self) -> DynDir {
|
|
|
|
DynDir {
|
|
|
|
arc: Arc::new(self),
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub trait AnchoredAtExt {
|
|
|
|
fn anchored_at(self, at: VPathBuf) -> Anchored<Self>
|
|
|
|
where
|
|
|
|
Self: Sized;
|
|
|
|
}
|
|
|
|
|
|
|
|
impl<T> AnchoredAtExt for T
|
|
|
|
where
|
2024-11-17 22:34:43 +01:00
|
|
|
T: Dir,
|
2024-11-16 18:33:41 +01:00
|
|
|
{
|
|
|
|
fn anchored_at(self, at: VPathBuf) -> Anchored<Self> {
|
|
|
|
Anchored::new(self, at)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-29 20:03:32 +01:00
|
|
|
/// 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 {
|
2025-01-14 23:09:01 +01:00
|
|
|
/// Media type string. <https://en.wikipedia.org/wiki/Media_type>
|
|
|
|
kind: String,
|
2024-11-29 20:03:32 +01:00
|
|
|
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 {
|
2025-01-14 23:09:01 +01:00
|
|
|
pub fn new(kind: impl Into<String>, bytes: Vec<u8>) -> Self {
|
|
|
|
Self {
|
|
|
|
kind: kind.into(),
|
|
|
|
bytes,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn kind(&self) -> &str {
|
|
|
|
&self.kind
|
2024-11-29 20:03:32 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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()
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
pub fn walk_dir_rec(dir: &dyn Dir, path: &VPath, f: &mut dyn FnMut(&VPath) -> ControlFlow<(), ()>) {
|
2024-11-29 20:03:32 +01:00
|
|
|
for entry in entries(dir, path) {
|
|
|
|
match f(&entry) {
|
2024-11-16 18:33:41 +01:00
|
|
|
ControlFlow::Continue(_) => (),
|
|
|
|
ControlFlow::Break(_) => return,
|
|
|
|
}
|
2024-11-29 20:03:32 +01:00
|
|
|
walk_dir_rec(dir, &entry, f);
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-17 22:34:43 +01:00
|
|
|
pub fn url(site: &str, dir: &dyn Dir, path: &VPath) -> Option<String> {
|
2024-11-29 20:03:32 +01:00
|
|
|
let anchor = query::<Anchor>(dir, path)?;
|
|
|
|
if let Some(version) = query::<ContentVersion>(dir, path) {
|
|
|
|
Some(format!("{}/{}?v={}", site, anchor.path, version.string))
|
2024-11-16 18:33:41 +01:00
|
|
|
} else {
|
2024-11-29 20:03:32 +01:00
|
|
|
Some(format!("{}/{}", site, anchor.path))
|
2024-11-16 18:33:41 +01:00
|
|
|
}
|
2024-11-08 14:52:32 +01:00
|
|
|
}
|