haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::fs::{self, DirBuilder, File};
use std::os::unix::fs::DirBuilderExt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};

#[cfg(test)]
use super::fixtures::{Event, Hooks};
use super::types::Error;

static SEQUENCE: AtomicU64 = AtomicU64::new(0);

pub(super) struct Stage {
    path: PathBuf,
    pub published: bool,
}

impl Stage {
    pub(super) fn create(
        destination: &Path,
        #[cfg(test)] hooks: &mut Hooks,
    ) -> Result<Self, Error> {
        let parent = destination
            .parent()
            .ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?;
        for _ in 0..64 {
            let sequence = SEQUENCE.fetch_add(1, Ordering::Relaxed);
            let path = parent.join(format!(".haem-stage-{}-{sequence}", std::process::id()));
            match DirBuilder::new().mode(0o700).create(&path) {
                Ok(()) => {
                    #[cfg(test)]
                    hooks.events.push(Event::Stage(path.clone()));
                    return Ok(Self {
                        path,
                        published: false,
                    });
                }
                Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
                Err(error) => return Err(Error::new("bundle_write_failed", &error)),
            }
        }
        Err(Error::new(
            "bundle_write_failed",
            "all staging names are occupied",
        ))
    }

    pub(super) fn path(&self) -> &Path {
        &self.path
    }

    pub(super) fn publish(
        &mut self,
        destination: &Path,
        #[cfg(test)] hooks: &mut Hooks,
    ) -> Result<(), Error> {
        #[cfg(test)]
        hooks
            .before_publish(destination)
            .map_err(|error| Error::new("bundle_publish_failed", &error))?;
        fs::rename(&self.path, destination)
            .map_err(|error| Error::new("bundle_publish_failed", &error))?;
        self.published = true;
        #[cfg(test)]
        hooks.events.push(Event::Publish(destination.to_owned()));
        sync_path(
            destination
                .parent()
                .ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?,
            #[cfg(test)]
            hooks,
        )
    }

    pub(super) fn cleanup(&self, #[cfg(test)] hooks: &mut Hooks) -> Result<(), Error> {
        if self.published {
            return Ok(());
        }
        #[cfg(test)]
        hooks
            .before_cleanup()
            .map_err(|error| Error::new("bundle_cleanup_failed", &error))?;
        fs::remove_dir_all(&self.path)
            .map_err(|error| Error::new("bundle_cleanup_failed", &error))?;
        sync_path(
            self.path
                .parent()
                .ok_or_else(|| Error::new("bundle_path_invalid", "staging parent missing"))?,
            #[cfg(test)]
            hooks,
        )
        .map_err(|error| Error::new("bundle_cleanup_failed", &error))
    }
}

pub(super) fn sync_path(path: &Path, #[cfg(test)] hooks: &mut Hooks) -> Result<(), Error> {
    let file = File::open(path).map_err(|error| Error::new("bundle_sync_failed", &error))?;
    sync_file(
        &file,
        path,
        #[cfg(test)]
        hooks,
    )
}

pub(super) fn sync_file(
    file: &File,
    path: &Path,
    #[cfg(test)] hooks: &mut Hooks,
) -> Result<(), Error> {
    #[cfg(test)]
    hooks
        .before_sync()
        .map_err(|error| Error::new("bundle_sync_failed", &error))?;
    file.sync_all().map_err(|error| {
        Error::new(
            "bundle_sync_failed",
            &format!("{}: {error}", path.display()),
        )
    })?;
    #[cfg(test)]
    hooks.events.push(Event::Sync(path.to_owned()));
    Ok(())
}

pub(super) fn stage_label(path: &Path) -> String {
    path.to_str().map_or_else(
        || format!("[lossy] {}", path.to_string_lossy()),
        str::to_owned,
    )
}

pub(super) fn stale_stages(destination: &Path) -> Result<Vec<String>, Error> {
    let parent = destination
        .parent()
        .ok_or_else(|| Error::new("bundle_path_invalid", "bundle parent missing"))?;
    let mut stages = Vec::new();
    let result: Result<(), Error> = (|| {
        for entry in
            fs::read_dir(parent).map_err(|error| Error::new("bundle_read_failed", &error))?
        {
            let entry = entry.map_err(|error| Error::new("bundle_read_failed", &error))?;
            if entry
                .file_name()
                .as_encoded_bytes()
                .starts_with(b".haem-stage-")
            {
                let path = entry.path();
                stages.push(stage_label(&path));
            }
        }
        Ok(())
    })();
    stages.sort_unstable();
    if let Err(error) = result {
        return Err(error.with_stale_stages(stages));
    }
    Ok(stages)
}