1use std::path::{PathBuf, Path};
2use std::fs::{create_dir_all, rename, remove_dir_all, canonicalize, File};
3use std::io::{Write, Read};
4use structopt::StructOpt;
5use confy;
6use serde::{Serialize, Deserialize};
7use walkdir::{WalkDir, DirEntry};
8use indicatif::ProgressBar;
9use regex::{Regex, RegexBuilder};
10
11mod default;
12
13
14#[derive(StructOpt)]
16pub struct Options {
18 #[structopt(short, long)]
19 pub recursive: bool,
20
21 #[structopt(short="H", long)]
22 pub hidden: bool,
24
25 #[structopt(short, long)]
26 pub verbose: bool,
28
29 #[structopt(short, long)]
30 pub quiet: bool,
32
33 #[structopt(long="dry-run")]
34 pub dry_run: bool,
36
37 #[structopt(short, long)]
38 pub undo: bool,
40
41 #[structopt(long="log", parse(from_os_str), default_value = "./organize-rt.log")]
42 pub log_path: PathBuf,
44
45 #[structopt(short = "s", long = "source", name="source", parse(from_os_str), required_unless = "undo")]
46 source_raw: Option<PathBuf>,
48
49 #[structopt(skip)]
50 pub source: PathBuf,
51
52 #[structopt(short = "o", long = "output", name="output", parse(from_os_str), required_unless = "undo")]
53 output_raw: Option<PathBuf>,
55
56 #[structopt(skip)]
57 pub output: PathBuf
58}
59
60
61impl Options {
62 pub fn verbose_print(&self, text: &str){
63 if self.verbose {
64 println!("{}", text);
65 }
66 }
67
68 pub fn default_print(&self, text: &str) {
69 if !self.quiet {
70 println!("{}", text);
71 }
72 }
73
74 pub fn resolve(&mut self) {
75 create_dir_all(&self.output_raw.as_ref().unwrap()).unwrap();
76 self.output = self.output_raw.clone().unwrap();
77 self.source = self.source_raw.clone().unwrap();
78 }
79}
80
81#[derive(Serialize, Deserialize)]
84struct RawRules {
85 rules: Vec<(String, String)>
86}
87
88impl Default for RawRules {
89 fn default() -> RawRules {
90 let mut rules = Vec::new();
91 default::rules(&mut rules);
92 RawRules {
93 rules
94 }
95 }
96}
97
98impl RawRules {
99 fn compile(self, output_dir: &PathBuf) -> Result<CompiledRules, Box<dyn std::error::Error>> {
100 let mut compiled_rules: Vec<(Regex, PathBuf)> = Vec::new();
101 for (regex, dir_name) in self.rules.into_iter() {
102 let regex = RegexBuilder::new(regex.as_str()).case_insensitive(true).build()?;
103 let mut path = (*output_dir).clone();
104 path.push(dir_name);
105 compiled_rules.push((regex, path));
106 }
107
108 Ok(CompiledRules{
109 rules: compiled_rules
110 })
111 }
112}
113
114
115pub struct CompiledRules {
116 rules: Vec<(Regex, PathBuf)>
117}
118
119impl CompiledRules {
120 pub fn iter(&self) -> std::slice::Iter<(Regex, PathBuf)> {
121 self.rules.iter()
122 }
123
124 pub fn load (options: &Options) -> Result<CompiledRules, Box<dyn std::error::Error>> {
125 let rawrules: RawRules = confy::load("organize-rt")?;
126 Ok(rawrules.compile(&options.output)?)
127 }
128}
129
130#[derive(Serialize, Deserialize)]
132struct Move {
133 from: PathBuf,
134 to: PathBuf
135}
136
137impl Move {
138 fn new(from: PathBuf, to: PathBuf) -> Move{
140 Move {
141 from,
142 to
143 }
144 }
145}
146
147pub fn get_files(hidden: bool, recursive: bool, source: &PathBuf) -> Vec<PathBuf> {
150 let mut walker = WalkDir::new(&source);
152 if !recursive {
153 walker = walker.max_depth(1);
154 }
155 let walker = walker.into_iter().filter_map(|e| e.ok())
156 .filter(|e| (hidden || !is_hidden(e)) && !e.file_type().is_dir());
157
158
159 let mut files: Vec<PathBuf> = Vec::new();
161 for entry in walker
162 {
163 files.push(entry.into_path());
164 }
165
166 files
167
168}
169
170fn is_hidden(entry: &DirEntry) -> bool {
171 entry.path()
172 .to_str()
173 .map(|s| s.contains("/."))
174 .unwrap_or(false)
175}
176
177pub fn create_dirs(options: &Options) -> Result<(), Box<dyn std::error::Error>>{
178 options.verbose_print("Creating dirs...");
179
180 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Audio")))?;
181 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Compressed")))?;
182 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Garbage")))?;
183 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Downloads")))?;
184 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Code")))?;
185 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Documents")))?;
186 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Images")))?;
187 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/ISO")))?;
188 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Configuration")))?;
189 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Encrypted")))?;
190 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Video")))?;
191 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/Unsorted")))?;
192 create_dir_all(Path::new(&(options.output.to_str().unwrap().to_owned() + "/REMOVE")))?;
193 options.verbose_print("Done!");
194
195 Ok(())
196}
197
198pub fn move_files(files: &Vec<PathBuf>, rules: &CompiledRules, options: &Options) {
199 let progressbar = ProgressBar::new(files.len() as u64);
200 let mut actions: Vec<Move> = Vec::new();
201
202 let mut id: u32 = 0;
203 for file in files {
204 id += 1;
205 for (regex, out_dir) in rules.iter() {
206 if regex.is_match(&file.file_name().unwrap().to_str().unwrap()) {
207 let mut file_out = out_dir.clone();
208 file_out.push(file.file_name().unwrap());
209
210 if !options.dry_run {
211 let file = canonicalize(&file).unwrap();
212
213 if file_out.exists() {
215 file_out.pop();
217 file_out.push(format!("{}.COPY{}", file.file_name().unwrap().to_str().unwrap(), id));
218 }
219
220 if let Err(e) = rename(&file, &file_out) {
222 options.default_print(format!("Failed to move file {} with error {}", file.to_str().unwrap(), e).as_str());
223 } else {
224 let file_out = canonicalize(&file_out).unwrap();
225 actions.push(Move::new(file, file_out));
226 }
227
228 } else{
229 options.default_print(format!("{} -> {}", file.to_str().unwrap(), file_out.to_str().unwrap()).as_str());
230 }
231
232 break;
233 }
234 }
235
236 if !options.quiet && !options.dry_run {
237 progressbar.inc(1);
238 }
239 }
240
241 let serialised_log = serde_json::to_string(&actions).unwrap();
243
244 let mut log_dir = options.log_path.clone();
246 log_dir.pop();
247 create_dir_all(log_dir).unwrap();
248
249 let mut file = File::create(&options.log_path).unwrap();
251 file.write(serialised_log.as_bytes()).unwrap();
252
253
254 if !options.dry_run {
256 options.verbose_print("Removing REMOVE dir...");
257 remove_dir_all(options.output.to_str().unwrap().to_owned() + "/REMOVE").unwrap();
258 options.verbose_print("Done!");
259 }
260}
261
262pub fn undo(options: &Options) {
265 let mut file = File::open(&options.log_path).unwrap();
266 let mut content = String::new();
267 file.read_to_string(&mut content).unwrap();
268
269 let actions:Vec<Move> = serde_json::from_str(&content).unwrap();
270
271 for action in actions {
272 let mut from_dir = action.from.clone();
273 from_dir.pop();
274 create_dir_all(from_dir).unwrap();
275 if !options.dry_run {
276 if let Err(e) = rename(&action.to, &action.from) {
277 options.default_print(format!("Failed to move {} back to {} with error '{}' (skipped it)",
278 action.to.to_str().unwrap(), action.from.to_str().unwrap(), e).as_str());
279 }
280 } else {
281 options.default_print(format!("{} -> {}", action.to.to_str().unwrap(), action.from.to_str().unwrap()).as_str());
282 }
283 }
284}