ghee 0.6.1

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

use std::{collections::BTreeSet, fs::remove_file, path::PathBuf};

use crate::{containing_table_info, walk::walk_paths, xattr_values};

#[derive(Error, Debug)]
pub enum RmErr {
    #[error("IO error removing xattrs: {0}")]
    IoError(std::io::Error),
}

/** Remove xattrs from `paths`.
 *
 * Not to be mistaken for `del` which removes records from tables.
 */
pub fn rm(
    paths: &Vec<PathBuf>,
    fields: &Vec<Xattr>,
    recursive: bool,
    force: bool,
    verbose: bool,
) -> Result<()> {
    if verbose {
        let fields: Vec<String> = fields.iter().map(|f| f.to_string()).collect();
        eprintln!("Removing {} from:", fields.join(", "));
    }

    for path in paths {
        let table_info = containing_table_info(path)?;

        for entry in walk_paths(path, &vec![], recursive, true)? {
            let entry = entry?;

            let xattrs = xattr_values(entry.path())?;

            let fields: Vec<Xattr> = if fields.is_empty() {
                xattrs.keys().cloned().collect()
            } else {
                fields.clone()
            };

            // The indices that the record is falling out of due to the removed fields, if any
            let mut exited_indices: BTreeSet<Key> = BTreeSet::new();

            for field in fields.iter() {
                let field_osstring = field.to_osstring();
                let result = xattr::remove(entry.path(), field_osstring).map_err(RmErr::IoError);
                if force {
                    // Just accept
                    result.unwrap_or_default();
                } else {
                    // Propagate errors
                    result?;
                }

                if let Some(table_info) = table_info.as_ref() {
                    for key in table_info.indices_abs().keys() {
                        if key.subkeys.contains(field) && xattrs.contains_key(field) {
                            exited_indices.insert(key.clone());
                        }
                    }
                }
            }

            if let Some(table_info) = table_info.as_ref() {
                for key in exited_indices {
                    let mut path = table_info.index_path_abs(&key).clone();

                    for subkey_value in key.value_for_record(&xattrs)? {
                        path.push(subkey_value.to_string());
                    }

                    remove_file(path).map_err(RmErr::IoError)?;
                }
            }

            if verbose {
                eprintln!("\t{}", entry.path().display());
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod test {
    use crate::{cmd::rm, test_support::Scenario, xattr_values};

    #[test]
    fn test_rm() {
        let s = Scenario::new("rm");

        let path1 = &s.dir1path1;
        let path2 = &s.dir2path1;

        assert!(path1.exists());
        assert!(path2.exists());

        {
            let values1 = xattr_values(path1).unwrap();

            assert!(values1.contains_key(&s.xattr1));
            assert!(values1.contains_key(&s.xattr2));
            assert!(values1.contains_key(&s.xattr3));
        }
        {
            let values2 = xattr_values(path2).unwrap();

            assert!(values2.contains_key(&s.xattr1));
            assert!(values2.contains_key(&s.xattr2));
            assert!(values2.contains_key(&s.xattr3));
        }

        // Remove
        rm(
            &vec![path1.clone()],
            &vec![s.xattr1.clone()],
            false,
            false,
            false,
        )
        .unwrap();

        assert!(
            !path1.exists(),
            "Record not removed from index even though it no longer has relevant xattr"
        );
        assert!(path2.exists());

        {
            let values2 = xattr_values(path2).unwrap();

            assert!(!values2.contains_key(&s.xattr1));
            assert!(values2.contains_key(&s.xattr2));
            assert!(values2.contains_key(&s.xattr3));
        }
    }

    #[test]
    fn test_rm_recursive() {
        let s = Scenario::new("rm-recursive");

        let path1 = {
            let mut path = s.dir1.clone();
            path.push("0");
            path
        };

        let path2 = {
            let mut path = s.dir2.clone();
            path.push("1");
            path.push("2");
            path
        };

        let path3 = {
            let mut path = s.dir1.clone();
            path.push("10");
            path
        };

        let path4 = {
            let mut path = s.dir2.clone();
            path.push("11");
            path.push("12");
            path
        };

        assert!(path1.exists());
        assert!(path2.exists());
        assert!(path3.exists());
        assert!(path4.exists());

        for path in vec![&path1, &path2, &path3, &path4] {
            let values = xattr_values(&path).unwrap();

            assert!(values.contains_key(&s.xattr1));
            assert!(values.contains_key(&s.xattr2));
            assert!(values.contains_key(&s.xattr3));
        }

        // Remove
        rm(
            &vec![s.dir1.clone()],
            &vec![s.xattr1.clone()],
            true,
            true,
            false,
        )
        .unwrap();

        assert!(
            !path1.exists(),
            "Record not removed from index even though it no longer has relevant xattr"
        );
        assert!(path2.exists());
        assert!(
            !path3.exists(),
            "Record not removed from index even though it no longer has relevant xattr"
        );
        assert!(path4.exists());
    }
}