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),
}
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?;
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()
});
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)?;
}
}
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");
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());
}
}