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}