kbolt-core 0.1.7

Core engine for kbolt local-first retrieval
Documentation
use super::{
    collection_ignore_file_path, count_ignore_patterns, validate_ignore_pattern, Engine,
    IgnoreListEntry,
};
use crate::lock::LockMode;
use crate::Result;
use kbolt_types::KboltError;

impl Engine {
    pub fn read_collection_ignore(
        &self,
        space: Option<&str>,
        collection: &str,
    ) -> Result<(String, Option<String>)> {
        let _lock = self.acquire_operation_lock(LockMode::Shared)?;
        let resolved_space = self.resolve_space_row(space, Some(collection))?;
        self.storage.get_collection(resolved_space.id, collection)?;

        let path =
            collection_ignore_file_path(&self.config.config_dir, &resolved_space.name, collection);
        let raw = match std::fs::read_to_string(path) {
            Ok(raw) => raw,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                return Ok((resolved_space.name, None))
            }
            Err(err) => return Err(err.into()),
        };

        let trimmed = raw.trim_end_matches('\n').to_string();
        if trimmed.trim().is_empty() {
            return Ok((resolved_space.name, None));
        }

        Ok((resolved_space.name, Some(trimmed)))
    }

    pub fn add_collection_ignore_pattern(
        &self,
        space: Option<&str>,
        collection: &str,
        pattern: &str,
    ) -> Result<(String, String)> {
        let _lock = self.acquire_operation_lock(LockMode::Exclusive)?;
        let resolved_space = self.resolve_space_row(space, Some(collection))?;
        self.storage.get_collection(resolved_space.id, collection)?;

        let normalized_pattern = validate_ignore_pattern(pattern)?;
        let path =
            collection_ignore_file_path(&self.config.config_dir, &resolved_space.name, collection);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }

        let mut file = std::fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&path)?;
        use std::io::Write;
        writeln!(file, "{normalized_pattern}")?;

        Ok((resolved_space.name, normalized_pattern))
    }

    pub fn remove_collection_ignore_pattern(
        &self,
        space: Option<&str>,
        collection: &str,
        pattern: &str,
    ) -> Result<(String, usize)> {
        let _lock = self.acquire_operation_lock(LockMode::Exclusive)?;
        let resolved_space = self.resolve_space_row(space, Some(collection))?;
        self.storage.get_collection(resolved_space.id, collection)?;

        let normalized_pattern = validate_ignore_pattern(pattern)?;
        let path =
            collection_ignore_file_path(&self.config.config_dir, &resolved_space.name, collection);
        let raw = match std::fs::read_to_string(&path) {
            Ok(raw) => raw,
            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
                return Ok((resolved_space.name, 0))
            }
            Err(err) => return Err(err.into()),
        };

        let mut removed_count = 0usize;
        let mut remaining = Vec::new();
        for line in raw.lines() {
            if line == normalized_pattern {
                removed_count = removed_count.saturating_add(1);
            } else {
                remaining.push(line.to_string());
            }
        }

        if removed_count == 0 {
            return Ok((resolved_space.name, 0));
        }

        if remaining.is_empty() {
            std::fs::remove_file(path)?;
            return Ok((resolved_space.name, removed_count));
        }

        let mut content = remaining.join("\n");
        content.push('\n');
        std::fs::write(path, content)?;
        Ok((resolved_space.name, removed_count))
    }

    pub fn list_collection_ignores(&self, space: Option<&str>) -> Result<Vec<IgnoreListEntry>> {
        let _lock = self.acquire_operation_lock(LockMode::Shared)?;
        let (space_id_filter, spaces_by_id) = if let Some(space_name) = space {
            let resolved = self.resolve_space_row(Some(space_name), None)?;
            let mut map = std::collections::HashMap::new();
            map.insert(resolved.id, resolved.name.clone());
            (Some(resolved.id), map)
        } else {
            let spaces = self.storage.list_spaces()?;
            let map = spaces
                .into_iter()
                .map(|space| (space.id, space.name))
                .collect::<std::collections::HashMap<_, _>>();
            (None, map)
        };

        let collections = self.storage.list_collections(space_id_filter)?;
        let mut entries = Vec::new();
        for collection in collections {
            let space_name = spaces_by_id
                .get(&collection.space_id)
                .ok_or_else(|| {
                    KboltError::Internal(format!(
                        "missing space mapping for collection '{}'",
                        collection.name
                    ))
                })?
                .clone();
            let path =
                collection_ignore_file_path(&self.config.config_dir, &space_name, &collection.name);
            if !path.is_file() {
                continue;
            }

            let raw = std::fs::read_to_string(path)?;
            entries.push(IgnoreListEntry {
                space: space_name,
                collection: collection.name,
                pattern_count: count_ignore_patterns(&raw),
            });
        }
        entries.sort_by(|left, right| {
            left.space
                .cmp(&right.space)
                .then(left.collection.cmp(&right.collection))
        });
        Ok(entries)
    }

    pub fn prepare_collection_ignore_edit(
        &self,
        space: Option<&str>,
        collection: &str,
    ) -> Result<(String, std::path::PathBuf)> {
        let _lock = self.acquire_operation_lock(LockMode::Exclusive)?;
        let resolved_space = self.resolve_space_row(space, Some(collection))?;
        self.storage.get_collection(resolved_space.id, collection)?;

        let path =
            collection_ignore_file_path(&self.config.config_dir, &resolved_space.name, collection);
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let _file = std::fs::OpenOptions::new()
            .create(true)
            .write(true)
            .truncate(false)
            .open(&path)?;

        Ok((resolved_space.name, path))
    }
}