Simple tool for automatic file management
1use std::fmt; 2use std::path::{Path, PathBuf}; 3use std::process::Command; 4 5use serde::{de, Deserialize}; 6use wildmatch::WildMatch; 7 8/// Definition of the job files 9#[derive(Debug, Deserialize)] 10pub struct Job { 11 pattern: Pattern, 12 location: PathBuf, 13 actions: Vec<Action>, 14} 15 16impl Job { 17 pub fn run(&self) { 18 let loc = normalise_path(&self.location); 19 20 for entry in loc.read_dir().unwrap() { 21 let entry = entry.unwrap(); 22 23 if self.pattern.matches(entry.file_name().as_ref()) { 24 self.execute_actions(&loc, entry.path().as_ref()); 25 } 26 } 27 } 28 29 fn execute_actions(&self, loc: &Path, path: &Path) { 30 for action in &self.actions { 31 action.execute(loc, path); 32 } 33 } 34} 35 36/// Patterns that can be matched against files 37#[derive(Debug, Deserialize)] 38pub enum Pattern { 39 #[serde(deserialize_with = "deserialize_wildcard")] 40 Wildcard(WildMatch), 41 #[serde(deserialize_with = "deserialize_regex")] 42 Regex(regex::Regex), 43 #[serde(untagged)] 44 Exact(Box<Path>), 45} 46 47impl Pattern { 48 pub fn matches(&self, path: &Path) -> bool { 49 match *self { 50 Self::Wildcard(ref pattern) => pattern.matches(path.to_str().unwrap()), 51 Self::Regex(ref pattern) => pattern.is_match(path.to_str().unwrap()), 52 Self::Exact(ref pattern) => **pattern == *path, 53 } 54 } 55} 56 57/// Actions available for file 58#[derive(Debug, Deserialize)] 59#[serde(untagged)] 60pub enum Action { 61 /// Run given script with 1st argument. It will be ran in parent directory for given file 62 Script { script: Box<Path> }, 63 /// Move given file to new destination 64 Move { move_to: Box<Path> }, 65 /// Print message and do nothing 66 Echo { message: String }, 67} 68 69impl Action { 70 pub fn execute(&self, dir: &Path, source: &Path) { 71 match *self { 72 Action::Script { ref script } => { 73 Command::new(script.as_ref()) 74 .arg(source) 75 .current_dir(dir) 76 .spawn() 77 .expect("Couldnt spawn process") 78 .wait() 79 .expect("Child exited abnormally"); 80 } 81 82 Action::Move { move_to: ref dest_dir } => { 83 let dest = normalise_path(dest_dir).join(source.file_name().unwrap()); 84 if let Err(err) = std::fs::rename(source, &dest) { 85 if err.kind() == std::io::ErrorKind::CrossesDevices { 86 panic!("X dev"); 87 } else { 88 panic!("Cannot move {source:?} -> {dest:?}: {err:?}"); 89 } 90 } 91 } 92 93 Action::Echo { ref message } => println!("{source:?} - {message}"), 94 } 95 } 96} 97 98fn normalise_path(path: &Path) -> PathBuf { 99 match path.strip_prefix("~") { 100 Ok(prefix) => std::env::home_dir().unwrap().join(prefix), 101 Err(_) => path.to_owned(), 102 } 103} 104 105struct RegexVisitor; 106 107impl<'de> de::Visitor<'de> for RegexVisitor { 108 type Value = regex::Regex; 109 110 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 111 write!(formatter, "expected regex string") 112 } 113 114 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> 115 where 116 E: de::Error, 117 { 118 match regex::Regex::new(s) { 119 Ok(regex) => Ok(regex), 120 Err(regex::Error::Syntax(ref desc)) => { 121 Err(E::invalid_value(de::Unexpected::Str(s), &desc.as_str())) 122 } 123 Err(_) => unreachable!(), 124 } 125 } 126} 127 128fn deserialize_regex<'de, D>(des: D) -> Result<regex::Regex, D::Error> 129where 130 D: serde::Deserializer<'de>, 131{ 132 des.deserialize_str(RegexVisitor) 133} 134 135struct WildcardVisitor; 136 137impl<'de> de::Visitor<'de> for WildcardVisitor { 138 type Value = WildMatch; 139 140 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 141 write!(formatter, "expected regex string") 142 } 143 144 fn visit_str<E>(self, s: &str) -> Result<Self::Value, E> 145 where 146 E: de::Error, 147 { 148 Ok(WildMatch::new(s)) 149 } 150} 151 152fn deserialize_wildcard<'de, D>(des: D) -> Result<WildMatch, D::Error> 153where 154 D: serde::Deserializer<'de>, 155{ 156 des.deserialize_str(WildcardVisitor) 157}