kcode-web-libs 0.2.0

Managed plain HTML, CSS, and JavaScript libraries with browser checks and immutable publication
Documentation
use crate::model::{File, ValidatedTree, validate_module_name, validate_tree};
use crate::storage;
use crate::{Error, Result};
use fs2::FileExt;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use uuid::Uuid;

pub(crate) fn publish(
    root: &Path,
    name: &str,
    files: &[File],
    validated: &ValidatedTree,
) -> Result<()> {
    validate_module_name(name)?;
    let supplied = validate_tree(files, name)?;
    if supplied.source_sha256 != validated.source_sha256 || supplied.manifest != validated.manifest
    {
        return Err(Error::new(
            "invalid_source: publication source does not match the validated source",
        ));
    }

    let module_parent =
        ensure_durable_child_directory(root, "module", "published module parent directory")?;
    let module_root =
        ensure_durable_child_directory(&module_parent, name, "published module directory")?;

    let locks_root = ensure_durable_child_directory(root, ".locks", "publication lock directory")?;
    let module_lock_root =
        ensure_durable_child_directory(&locks_root, name, "module publication lock directory")?;
    let lock_path = module_lock_root.join(".lock");
    match fs::symlink_metadata(&lock_path) {
        Ok(metadata) if metadata.file_type().is_symlink() || !metadata.is_file() => {
            return Err(Error::new(
                "invalid_publication_storage: publication lock is not a regular file",
            ));
        }
        Ok(_) => {}
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => return Err(Error::io("inspect publication lock", error)),
    }
    let lock = OpenOptions::new()
        .create(true)
        .truncate(false)
        .read(true)
        .write(true)
        .open(&lock_path)
        .map_err(|error| Error::io("open publication lock", error))?;
    if !lock
        .metadata()
        .map_err(|error| Error::io("inspect opened publication lock", error))?
        .is_file()
    {
        return Err(Error::new(
            "invalid_publication_storage: opened publication lock is not a regular file",
        ));
    }
    FileExt::lock_exclusive(&lock).map_err(|error| Error::io("lock module publication", error))?;

    let version_directory = format!("v{}", validated.manifest.version);
    let destination = module_root.join(&version_directory);
    if path_exists(&destination)? {
        return Err(Error::new(format!(
            "already_published: `{name}` version `{}` already exists",
            validated.manifest.version
        )));
    }

    let staging = module_root.join(format!(".staging-{}", Uuid::new_v4()));
    fs::create_dir(&staging)
        .map_err(|error| Error::io("create staged publication directory", error))?;

    if let Err(error) = write_publication(&staging, files) {
        remove_staging(&staging, &module_root);
        return Err(error);
    }

    match path_exists(&destination) {
        Ok(false) => {}
        Ok(true) => {
            remove_staging(&staging, &module_root);
            return Err(Error::new(format!(
                "already_published: `{name}` version `{}` already exists",
                validated.manifest.version
            )));
        }
        Err(error) => {
            remove_staging(&staging, &module_root);
            return Err(error);
        }
    }

    if let Err(error) = fs::rename(&staging, &destination) {
        remove_staging(&staging, &module_root);
        return Err(Error::io("install published module", error));
    }

    if let Err(error) = storage::sync_directory(&module_root) {
        return Err(Error::new(format!(
            "publish.commit_uncertain: `{name}` version `{}` was installed but the published \
             module directory could not be synchronized: {error}",
            validated.manifest.version
        )));
    }

    drop(lock);

    Ok(())
}

pub(crate) fn root_path(path: &Path, create: bool) -> Result<PathBuf> {
    if create {
        storage::create_dir_all_durable(path, "create publication root")?;
    }
    let absolute = if path.is_absolute() {
        path.to_path_buf()
    } else {
        std::env::current_dir()
            .map_err(|error| Error::io("read current directory", error))?
            .join(path)
    };
    let metadata = fs::symlink_metadata(&absolute)
        .map_err(|error| Error::io("inspect publication root", error))?;
    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        return Err(Error::new(format!(
            "invalid_publication_storage: publication root is not a regular directory: {}",
            absolute.display()
        )));
    }
    fs::canonicalize(&absolute).map_err(|error| Error::io("canonicalize publication root", error))
}

fn ensure_durable_child_directory(parent: &Path, child: &str, label: &str) -> Result<PathBuf> {
    ensure_regular_directory(parent, "publication directory parent")?;
    let path = parent.join(child);

    match fs::symlink_metadata(&path) {
        Ok(_) => {
            ensure_regular_directory(&path, label)?;
            return Ok(path);
        }
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
        Err(error) => return Err(Error::io(&format!("inspect {label}"), error)),
    }

    match fs::create_dir(&path) {
        Ok(()) => {
            ensure_regular_directory(&path, label)?;
            storage::sync_directory(parent)?;
            Ok(path)
        }
        Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
            ensure_regular_directory(&path, label)?;
            Ok(path)
        }
        Err(error) => Err(Error::io(&format!("create {label}"), error)),
    }
}

fn write_publication(root: &Path, files: &[File]) -> Result<()> {
    for file in files {
        let destination = root.join(&file.path);
        if let Some(parent) = destination.parent() {
            fs::create_dir_all(parent)
                .map_err(|error| Error::io("create published source directory", error))?;
        }

        let mut output = OpenOptions::new()
            .create_new(true)
            .write(true)
            .open(&destination)
            .map_err(|error| Error::io("create published source file", error))?;
        output
            .write_all(file.contents.as_bytes())
            .map_err(|error| Error::io("write published source file", error))?;
        output
            .sync_all()
            .map_err(|error| Error::io("sync published source file", error))?;
    }

    storage::sync_tree_directories(root)
}

fn remove_staging(staging: &Path, module_root: &Path) {
    if fs::remove_dir_all(staging).is_ok() {
        let _ = storage::sync_directory(module_root);
    }
}

fn ensure_regular_directory(path: &Path, label: &str) -> Result<()> {
    let metadata = fs::symlink_metadata(path)
        .map_err(|error| Error::io(&format!("inspect {label}"), error))?;
    if !metadata.is_dir() || metadata.file_type().is_symlink() {
        return Err(Error::new(format!(
            "invalid_publication_storage: {label} is not a regular directory"
        )));
    }
    Ok(())
}

fn path_exists(path: &Path) -> Result<bool> {
    match fs::symlink_metadata(path) {
        Ok(_) => Ok(true),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
        Err(error) => Err(Error::io("inspect publication destination", error)),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{File, create};
    use tempfile::TempDir;

    fn source_root(root: &TempDir) -> PathBuf {
        root.path().join("source")
    }

    fn publications_root(root: &TempDir) -> PathBuf {
        root.path().join("publications")
    }

    #[test]
    fn installs_one_immutable_exact_version() {
        let root = TempDir::new().unwrap();
        let publications = publications_root(&root);
        let library = create(source_root(&root), &publications, "example").unwrap();
        let validated = validate_tree(&library.files, "example").unwrap();

        publish(&publications, "example", &library.files, &validated).unwrap();
        assert_eq!(
            fs::read_to_string(publications.join("module/example/v0.1.0/index.js")).unwrap(),
            library
                .files
                .iter()
                .find(|file| file.path == "index.js")
                .unwrap()
                .contents
        );

        let error = publish(&publications, "example", &library.files, &validated).unwrap_err();
        assert!(error.to_string().starts_with("already_published:"));
    }

    #[test]
    fn rejects_source_that_does_not_match_the_validated_tree() {
        let root = TempDir::new().unwrap();
        let publications = publications_root(&root);
        let library = create(source_root(&root), &publications, "example").unwrap();
        let validated = validate_tree(&library.files, "example").unwrap();
        let mut changed = library.files.clone();
        changed.push(File::new("extra.js", "export {};"));

        let error = publish(&publications, "example", &changed, &validated).unwrap_err();
        assert!(error.to_string().starts_with("invalid_source:"));
        assert!(!publications.join("module/example/v0.1.0").exists());
    }

    #[test]
    fn installs_distinct_versions_as_complete_independent_copies() {
        let root = TempDir::new().unwrap();
        let publications = publications_root(&root);
        let library = create(source_root(&root), &publications, "example").unwrap();
        let first = validate_tree(&library.files, "example").unwrap();
        publish(&publications, "example", &library.files, &first).unwrap();

        let mut second_files = library.files.clone();
        second_files
            .iter_mut()
            .find(|file| file.path == "kcode-web.json")
            .unwrap()
            .contents =
            r#"{"name":"example","version":"0.2.0","entry":"index.js","tests":"tests.js"}"#
                .to_owned();
        second_files
            .iter_mut()
            .find(|file| file.path == "index.js")
            .unwrap()
            .contents = "export const release = \"second\";".to_owned();
        let second = validate_tree(&second_files, "example").unwrap();
        publish(&publications, "example", &second_files, &second).unwrap();

        assert!(
            publications
                .join("module/example/v0.1.0/tests.js")
                .is_file()
        );
        assert_eq!(
            fs::read_to_string(publications.join("module/example/v0.2.0/index.js")).unwrap(),
            "export const release = \"second\";"
        );
        assert_ne!(
            fs::read_to_string(publications.join("module/example/v0.1.0/index.js")).unwrap(),
            fs::read_to_string(publications.join("module/example/v0.2.0/index.js")).unwrap()
        );
    }

    #[cfg(unix)]
    #[test]
    fn rejects_a_symlinked_module_parent_without_creating_outside_the_root() {
        use std::os::unix::fs::symlink;

        let root = TempDir::new().unwrap();
        let outside = TempDir::new().unwrap();
        let publications = publications_root(&root);
        let library = create(source_root(&root), &publications, "example").unwrap();
        let validated = validate_tree(&library.files, "example").unwrap();

        symlink(outside.path(), publications.join("module")).unwrap();

        let error = publish(&publications, "example", &library.files, &validated).unwrap_err();
        assert!(
            error
                .to_string()
                .starts_with("invalid_publication_storage:")
        );
        assert!(!outside.path().join("example").exists());
    }
}