organize_rt/
lib.rs

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// --CLI ARGS SECTION--
15#[derive(StructOpt)]
16///Tool for organizing files in garbage dirs like 'Downloads'. 
17pub struct Options {
18    #[structopt(short, long)]
19    pub recursive: bool,
20    
21    #[structopt(short="H", long)]
22    ///Include hidden files/directories
23    pub hidden: bool,
24    
25    #[structopt(short, long)]
26    ///Show more info
27    pub verbose: bool,
28    
29    #[structopt(short, long)]
30    ///Quiet run, empty output
31    pub quiet: bool,
32
33    #[structopt(long="dry-run")]
34    ///Prints where the file would move, but does not move
35    pub dry_run: bool,
36
37    #[structopt(short, long)]
38    ///Undo action (require log)
39    pub undo: bool,
40
41    #[structopt(long="log", parse(from_os_str), default_value = "./organize-rt.log")]
42    ///Path to save/load log
43    pub log_path: PathBuf,
44
45    #[structopt(short = "s", long = "source", name="source", parse(from_os_str), required_unless = "undo")]
46    ///Directory to organize
47    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 directory
54    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// --REGEX RULES SECTION--
82
83#[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// --LOG SECTION--
131#[derive(Serialize, Deserialize)]
132struct Move {
133    from: PathBuf,
134    to: PathBuf
135}
136
137impl Move {
138    //Resolve path into absolute
139    fn new(from: PathBuf, to: PathBuf) -> Move{
140        Move {
141            from,
142            to
143        }
144    }
145}
146
147// --NORMAL MAIN SECTION--
148
149pub fn get_files(hidden: bool, recursive: bool, source: &PathBuf) -> Vec<PathBuf> {
150    //Walker setup
151    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    //Walk
160    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                    //Check if file already exists
214                    if file_out.exists() {
215                        //and change it name
216                        file_out.pop();
217                        file_out.push(format!("{}.COPY{}", file.file_name().unwrap().to_str().unwrap(), id));
218                    }
219
220                    //Skip errors
221                    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    //Save log
242    let serialised_log = serde_json::to_string(&actions).unwrap();
243
244    // - Create log dir
245    let mut log_dir = options.log_path.clone();
246    log_dir.pop();
247    create_dir_all(log_dir).unwrap();
248
249    // - Write log
250    let mut file = File::create(&options.log_path).unwrap();
251    file.write(serialised_log.as_bytes()).unwrap();
252
253
254    //Delete `REMOVE` dir
255    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
262// --UNDO MAIN SECTION-- 
263
264pub 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}