kcode-web-libs 0.1.0

Managed plain HTML, CSS, and JavaScript libraries with browser checks and immutable publication
Documentation
use crate::{Error, Result};
use std::fs;
use std::path::{Path, PathBuf};

pub(crate) fn create_dir_all_durable(path: &Path, action: &str) -> Result<()> {
    let absolute = absolute_path(path)?;
    let existing_ancestor = nearest_existing_ancestor(&absolute, action)?;

    fs::create_dir_all(&absolute).map_err(|error| Error::io(action, error))?;

    let mut current = absolute;
    loop {
        sync_directory(&current)?;
        if current == existing_ancestor {
            break;
        }
        current = current.parent().map(Path::to_path_buf).ok_or_else(|| {
            Error::new("io: durable directory creation lost its existing ancestor")
        })?;
    }

    Ok(())
}

pub(crate) fn sync_tree_directories(root: &Path) -> Result<()> {
    let mut directories = vec![root.to_path_buf()];
    collect_directories(root, &mut directories)?;
    directories.sort_by_key(|path| std::cmp::Reverse(path.components().count()));

    for directory in directories {
        sync_directory(&directory)?;
    }
    Ok(())
}

pub(crate) fn sync_directory(path: &Path) -> Result<()> {
    let directory =
        fs::File::open(path).map_err(|error| Error::io("open directory for sync", error))?;
    directory
        .sync_all()
        .map_err(|error| Error::io("sync directory", error))
}

fn collect_directories(directory: &Path, output: &mut Vec<PathBuf>) -> Result<()> {
    let entries =
        fs::read_dir(directory).map_err(|error| Error::io("read directory for sync", error))?;

    for entry in entries {
        let entry = entry.map_err(|error| Error::io("read directory entry for sync", error))?;
        let metadata = entry
            .metadata()
            .map_err(|error| Error::io("inspect directory entry for sync", error))?;
        if metadata.is_dir() {
            let path = entry.path();
            output.push(path.clone());
            collect_directories(&path, output)?;
        }
    }
    Ok(())
}

fn absolute_path(path: &Path) -> Result<PathBuf> {
    if path.is_absolute() {
        return Ok(path.to_path_buf());
    }
    std::env::current_dir()
        .map(|current| current.join(path))
        .map_err(|error| Error::io("resolve durable directory path", error))
}

fn nearest_existing_ancestor(path: &Path, action: &str) -> Result<PathBuf> {
    let mut current = path.to_path_buf();
    loop {
        match fs::symlink_metadata(&current) {
            Ok(_) => return Ok(current),
            Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
                current = current.parent().map(Path::to_path_buf).ok_or_else(|| {
                    Error::new(format!(
                        "io: {action}: path has no existing filesystem ancestor"
                    ))
                })?;
            }
            Err(error) => return Err(Error::io(action, error)),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use tempfile::TempDir;

    #[test]
    fn durably_creates_nested_directories() {
        let root = TempDir::new().unwrap();
        let nested = root.path().join("one/two/three");

        create_dir_all_durable(&nested, "create nested test directory").unwrap();

        assert!(nested.is_dir());
    }
}