ghee 0.6.1

That thin layer of data change management over the filesystem
Documentation
use anyhow::{Context, Result};
use ghee_lang::{Assignment, Value, Xattr};
use path_absolutize::*;
use thiserror::Error;
use walkdir::WalkDir;

use crate::{containing_table_info, xattr_values, Record};

use std::collections::HashSet;
use std::fs::{create_dir_all, hard_link, remove_file};
use std::path::{Path, PathBuf};

#[derive(Error, Debug)]
pub enum SetErr {
    #[error("An IO error occurred while setting xattrs: {0}")]
    IoError(std::io::Error),

    #[error("Field assignment is to non-unique key value already taken by record at {0:?}")]
    NonUniqueKeyValue(PathBuf),
}

/** Set xattr values on paths */
pub fn set<P: AsRef<Path>>(
    paths: &Vec<P>,
    field_assignments: &Vec<Assignment>,
    recursive: bool,
    verbose: bool,
) -> Result<()> {
    let paths: Vec<PathBuf> = paths
        .iter()
        .map(|p| p.as_ref().absolutize().unwrap().to_path_buf())
        .collect();
    let max_depth = if recursive { usize::MAX } else { 0 };

    let field_assignments: Vec<(Xattr, Value)> = field_assignments
        .iter()
        .map(|fa| (fa.xattr.clone(), fa.value.clone()))
        .collect();

    for path in &paths {
        let table_info = containing_table_info(path)?;
        for entry in WalkDir::new(path).max_depth(max_depth).into_iter() {
            let entry = entry?;

            // Q Can this be done without reading the xattr values?
            // A It would depend on if all index key components
            //   can be inferred from entry.path(); this usually
            //   shouldn't be the case, meaning usually we will need
            //   to read the xattrs
            let old_record: Option<Record> = if table_info.is_some() {
                Some(xattr_values(entry.path())?)
            } else {
                None
            };

            let old_paths: Option<HashSet<PathBuf>> = table_info.as_ref().map(|table_info| {
                let old_record = old_record.as_ref().unwrap();
                table_info
                    .indices_abs()
                    .iter()
                    .filter_map(|(key, path)| key.path_for_record(path.clone(), old_record).ok())
                    .collect()
            });

            let new_record = {
                let mut new_record = old_record.clone();

                for (field, value) in field_assignments.iter() {
                    let field_osstring = field.to_osstring();

                    xattr::set(entry.path(), field_osstring, value.as_bytes().as_slice())
                        .map_err(SetErr::IoError)
                        .with_context(|| {
                            format!("Could not set xattr values on {}", entry.path().display())
                        })?;

                    new_record
                        .as_mut()
                        .map(|record| record.insert(field.clone(), value.clone()));

                    if verbose {
                        eprintln!("Set {} to {} on {}", field, value, path.display());
                    }
                }

                new_record
            };

            let new_paths: Option<HashSet<PathBuf>> = table_info.as_ref().map(|table_info| {
                let new_record = new_record.as_ref().unwrap();
                table_info
                    .indices_abs()
                    .iter()
                    .map(|(key, path)| key.path_for_record(path.clone(), new_record).unwrap())
                    .collect()
            });

            let paths_to_delete: Option<HashSet<PathBuf>> = old_paths.as_ref().map(|old_paths| {
                let new_paths = new_paths.as_ref().unwrap();
                old_paths.difference(new_paths).cloned().collect()
            });

            let paths_to_link: Option<HashSet<PathBuf>> = old_paths.as_ref().map(|old_paths| {
                let new_paths = new_paths.as_ref().unwrap();
                new_paths.difference(old_paths).cloned().collect()
            });

            // First, link
            if let Some(paths_to_link) = paths_to_link {
                for p in paths_to_link {
                    if p.exists() {
                        return Err(SetErr::NonUniqueKeyValue(p).into());
                    }

                    if let Some(parent) = p.parent() {
                        create_dir_all(parent)?;
                    }

                    debug_assert!(!p.exists());

                    hard_link(entry.path(), p)?;
                }
            }

            // Now, delete
            if let Some(paths_to_delete) = paths_to_delete {
                for p in paths_to_delete {
                    remove_file(p)?;
                }
            }
        }
    }

    Ok(())
}

#[cfg(test)]
mod test {
    use std::{
        env::{current_dir, set_current_dir},
        fs::File,
        path::PathBuf,
    };

    use ghee_lang::{parse_assignment, Value, Xattr};

    use crate::{
        cmd::ins::ins_records,
        test_support::{Scenario, TempDirAuto},
        xattr_values, Record,
    };

    use super::set;

    #[test]
    fn test_set_de_novo() {
        let s = Scenario::new("set-de-novo");

        let blah = Xattr::from("blah");

        {
            let values = xattr_values(&s.dir1path1).unwrap();
            assert!(!values.contains_key(&blah));
        }

        set(
            &vec![s.dir1path1.clone()],
            &vec![parse_assignment(b"blah=5").unwrap().1],
            false,
            false,
        )
        .unwrap();

        let values = xattr_values(&s.dir1path1).unwrap();

        assert!(values.contains_key(&blah));
        assert_eq!(values[&blah], Value::Number(5f64));
    }

    #[test]
    fn test_set_overwrite_indexed() {
        let s = Scenario::new("set-overwrite-indexed");

        // Indexed in dir2 but not dir1
        let attr = &s.xattr2;

        let (path1, path2) = {
            let values = xattr_values(&s.dir1path1).unwrap();
            assert!(values.contains_key(&attr));

            let path1 = s.key1.path_for_record(s.dir1.clone(), &values).unwrap();

            let path2 = s.key2.path_for_record(s.dir2.clone(), &values).unwrap();

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

            (path1, path2)
        };

        set(
            &vec![s.dir1path1.clone()],
            &vec![parse_assignment(b"test2=5").unwrap().1],
            false,
            false,
        )
        .unwrap();

        let values = xattr_values(&s.dir1path1).unwrap();

        assert!(values.contains_key(&attr));
        assert_eq!(values[&attr], Value::Number(5f64));

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

        let path3 = s.key2.path_for_record(s.dir2.clone(), &values).unwrap();

        assert!(path3.exists());
    }

    #[test]
    fn test_set_adding_to_index() {
        let s = Scenario::new("set-adding-to-index");

        let record = {
            let mut r = Record::new();
            r.insert(s.xattr1.clone(), Value::Number(55f64));
            r
        };

        let path1 = s.key1.path_for_record(s.dir1.clone(), &record).unwrap();

        assert!(!path1.exists());

        ins_records(&s.dir1, std::iter::once(record.clone()), false).unwrap();

        assert!(path1.exists());

        let mut record = record;

        record.insert(s.xattr2.clone(), Value::Number(100f64));
        record.insert(s.xattr3.clone(), Value::Number(101f64));

        let record = record;

        let path2 = s.key2.path_for_record(s.dir2.clone(), &record).unwrap();

        assert!(!path2.exists());

        set(
            &vec![path1],
            &vec![
                parse_assignment(b"test2=100").unwrap().1,
                parse_assignment(b"test3=101").unwrap().1,
            ],
            false,
            false,
        )
        .unwrap();

        assert!(path2.exists());
    }

    #[test]
    fn test_set_multiple_relative_path() {
        let prior = current_dir().unwrap();

        let dir = TempDirAuto::new("ghee-test-set-multiple-relative-path");

        set_current_dir(&dir).unwrap();

        let file = PathBuf::from("./README.txt");

        File::create(&file).unwrap();

        set(
            &vec![file],
            &vec![
                parse_assignment(b"name=Sandeep").unwrap().1,
                parse_assignment(b"id=2").unwrap().1,
                parse_assignment(b"state=CA").unwrap().1,
            ],
            true,
            false,
        )
        .unwrap();

        set_current_dir(prior).unwrap();
    }

    #[test]
    fn test_set_key_collision() {
        let s = Scenario::new("set-key-collision");

        assert!(set(
            &vec![s.dir1path2],
            &vec![parse_assignment(b"test1=0").unwrap().1],
            false,
            false,
        )
        .is_err());
    }
}