rofis 0.5.0

Read-only HTTP file server
Documentation
use orfail::OrFail;
use patricia_tree::PatriciaSet;
use std::path::{Path, PathBuf};

#[derive(Debug)]
pub struct DirsIndex {
    root: PathBuf,
    index: PatriciaSet,
}

impl DirsIndex {
    pub fn build<P: AsRef<Path>>(root: P) -> orfail::Result<Self> {
        let root = root.as_ref().canonicalize().or_fail()?;
        let mut index = PatriciaSet::new();
        index.insert("");

        let mut stack = vec![root.clone()];
        while let Some(dir) = stack.pop() {
            for entry in std::fs::read_dir(dir).or_fail()? {
                let entry = entry.or_fail()?;
                let file_type = entry.file_type().or_fail()?;
                let file_path = entry.path();
                if file_type.is_dir()
                    && file_path.file_name().is_some_and(|name| {
                        name.to_str().is_some_and(|name| !name.starts_with('.'))
                    })
                {
                    let relative_path = file_path
                        .strip_prefix(&root)
                        .or_fail()?
                        .to_str()
                        .or_fail()?;
                    index.insert(relative_path.bytes().rev().collect::<Vec<_>>());
                    stack.push(file_path);
                }
            }
        }
        Ok(Self { root, index })
    }

    pub fn find_dirs_by_suffix(&self, suffix: &str) -> Vec<PathBuf> {
        let mut dirs = Vec::new();
        let index_prefix = suffix.bytes().rev().collect::<Vec<_>>();
        for mut relative_path in self.index.iter_prefix(&index_prefix) {
            relative_path.reverse();
            dirs.push(
                self.root
                    .join(String::from_utf8(relative_path).expect("unreachable")),
            );
        }
        dirs
    }

    pub fn root_dir(&self) -> &PathBuf {
        &self.root
    }

    pub fn is_empty(&self) -> bool {
        self.index.is_empty()
    }

    pub fn len(&self) -> usize {
        self.index.len()
    }
}