ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use anyhow::Result;
use ghee_lang::Key;
use ghee_lang::Xattr;
use thiserror::Error;

use std::fs::hard_link;

use std::fs::create_dir_all;

use std::path::Path;

use walkdir::WalkDir;

use crate::declare_closure_indices;
use crate::paths::sub_idx_path;

use std::path::PathBuf;

use super::init;

#[derive(Error, Debug)]
pub enum IdxErr {
    #[error("IO error when getting xattr")]
    GetIoError(std::io::Error),

    #[error("Got empty value for xattr {0}")]
    GotEmptyValue(Xattr),

    #[error("Error creating directory {path}: {err}")]
    CreateDirIoError { path: PathBuf, err: std::io::Error },

    #[error("Error hard linking {original} to {link}")]
    HardLinkError {
        original: PathBuf,
        link: PathBuf,
        err: std::io::Error,
    },
}

/**
 * Create a new index of the data in `src`, locating the index in `dest`, or a subdirectory of `src` by default.
 *
 * The new index uses `keys` as its primary key.
 *
 * `dest` will have `src` and all _its_ related indices as related indices; and `src` and all its related indices
 * will be updated to include `dest` as an alternate index.
 */
pub fn idx(src: &PathBuf, dest: Option<&PathBuf>, keys: &Key, verbose: bool) -> Result<()> {
    if keys.is_empty() {
        panic!("No keys were provided to index by");
    }

    let dest = dest.cloned().unwrap_or_else(|| {
        sub_idx_path(src, keys).unwrap_or_else(|e| panic!("Could not get sub-index path: {}", e))
    });

    for entry in WalkDir::new(&src) {
        let entry = entry.unwrap_or_else(|e| panic!("Could not read directory: {}", e));

        if entry.file_type().is_dir() {
            continue;
        }

        let mut key = PathBuf::new();
        for maybe_subkey in keys.iter().map(|subkey| {
            let subkey_osstring = subkey.to_osstring();
            (xattr::get(entry.path(), subkey_osstring), subkey)
        }) {
            let subkey = maybe_subkey
                .0
                .map_err(IdxErr::GetIoError)?
                .ok_or(IdxErr::GotEmptyValue(maybe_subkey.1.clone()))?;

            let slice = &subkey[..];
            let str = String::from_utf8_lossy(slice);
            let str_ref = str.as_ref();
            let path = Path::new(str_ref);
            key.push(path);
        }

        let path = dest.join(key.as_path());

        let dir = path.parent().unwrap();

        create_dir_all(dir).map_err(|e| IdxErr::CreateDirIoError {
            path: dir.to_path_buf(),
            err: e,
        })?;

        hard_link(entry.path(), &path).map_err(|err| IdxErr::HardLinkError {
            original: entry.path().to_path_buf(),
            link: path.clone(),
            err,
        })?;

        if verbose {
            eprintln!("{} -> {}", entry.path().display(), path.display());
        }
    }

    init(&dest, &keys, false)?;

    declare_closure_indices(src, &dest)
}

#[cfg(test)]
mod test {
    use ghee_lang::Key;

    use crate::{cmd::init, table_info, test_support::TempDirAuto};

    use super::idx;

    #[test]
    fn test_idx() {
        let dir1 = TempDirAuto::new("ghee-test-idx:1");

        let dir2 = TempDirAuto::new("ghee-test-idx:2");

        let dir3 = TempDirAuto::new("ghee-test-idx:3");

        let key1 = Key::from(vec!["test1"]);

        let key2 = Key::from(vec!["test2"]);

        let key3 = Key::from_string("test3");

        init(&dir1, &key1, false).unwrap();

        {
            let info1 = table_info(&dir1).unwrap().unwrap();

            assert!(info1.indices_abs().contains_key(&key1));
            assert_eq!(info1.index_path_abs(&key1), &dir1.dir);
            assert_eq!(info1.indices_abs().len(), 1);
        }

        idx(&dir1, Some(&dir2), &key2, false).unwrap();

        {
            let info1 = table_info(&dir1).unwrap().unwrap();

            assert!(info1.indices_abs().contains_key(&key2));
            assert_eq!(info1.index_path_abs(&key2), &dir2.dir);
            assert_eq!(info1.indices_abs().len(), 2);

            assert_eq!(info1.key(), &key1);
        }

        {
            let info2 = table_info(&dir2).unwrap().unwrap();

            assert!(info2.indices_abs().contains_key(&key1));
            assert_eq!(info2.index_path_abs(&key1), &dir1.dir);
            assert_eq!(info2.indices_abs().len(), 2);

            assert_eq!(info2.key(), &key2);
        }

        idx(&dir1, Some(&dir3), &key3, false).unwrap();

        // Make sure the indices are updated properly after a second index
        // (no overwriting of the previous)
        let info = table_info(&dir1).unwrap().unwrap();
        let indices = info.indices_abs();

        assert_eq!(indices.len(), 3);
        assert!(indices.contains_key(&key1));
        assert!(indices.contains_key(&key2));
        assert!(indices.contains_key(&key3));
    }

    #[test]
    fn test_idx_chain() {
        let dir1 = TempDirAuto::new("ghee-test-idx-chain:1");

        let dir2 = TempDirAuto::new("ghee-test-idx-chain:2");

        let dir3 = TempDirAuto::new("ghee-test-idx-chain:3");

        let key1 = Key::from_string("test1");

        let key2 = Key::from_string("test2");

        let key3 = Key::from_string("test3");

        init(&dir1, &key1, false).unwrap();
        idx(&dir1, Some(&dir2), &key2, false).unwrap();
        idx(&dir2, Some(&dir3), &key3, false).unwrap();

        for dir in vec![&dir1, &dir2, &dir3] {
            let info = table_info(dir).unwrap().unwrap();

            for key in vec![&key1, &key2, &key3] {
                assert!(
                    info.indices_abs().contains_key(key),
                    "Table at {} does not contain key {:?}",
                    dir.display(),
                    key
                );
            }
        }
    }
}