-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from janpipek/split-modules
split-modules
- Loading branch information
Showing
4 changed files
with
437 additions
and
218 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
use crate::extensions::{find_extensions_from_content, has_correct_extension}; | ||
use colored::Colorize; | ||
use regex::Regex; | ||
use std::fmt::{Display, Formatter}; | ||
use std::path::{Path, PathBuf}; | ||
use unidecode::unidecode; | ||
|
||
#[derive(Clone)] | ||
pub struct RenameIntent { | ||
pub old_name: PathBuf, | ||
pub new_name: PathBuf, | ||
} | ||
|
||
impl RenameIntent { | ||
/// Is the new name different from the old one? | ||
pub fn is_changed(&self) -> bool { | ||
self.old_name != self.new_name | ||
} | ||
} | ||
|
||
impl Display for RenameIntent { | ||
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { | ||
if self.is_changed() { | ||
write!( | ||
f, | ||
"{0} → {1}", | ||
self.old_name.to_string_lossy().red(), | ||
self.new_name.to_string_lossy().green() | ||
) | ||
} else { | ||
write!(f, "{0} =", self.old_name.to_string_lossy(),) | ||
} | ||
} | ||
} | ||
|
||
pub trait RenameCommand { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf; | ||
|
||
fn suggest_renames(&self, files: &[PathBuf]) -> Vec<RenameIntent> { | ||
files | ||
.iter() | ||
.map(|path| RenameIntent { | ||
old_name: path.clone(), | ||
new_name: self.suggest_new_name(path), | ||
}) | ||
.collect() | ||
} | ||
} | ||
|
||
pub struct Normalize; | ||
|
||
impl RenameCommand for Normalize { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let path_str = old_name.to_string_lossy().to_string(); | ||
let new_name = unidecode(&path_str).replace(' ', "_"); //#.to_lowercase(); | ||
PathBuf::from(new_name) | ||
} | ||
} | ||
|
||
pub struct SetExtension { | ||
pub extension: String, | ||
} | ||
|
||
impl RenameCommand for SetExtension { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let mut new_name = old_name.to_path_buf(); | ||
new_name.set_extension(&self.extension); | ||
new_name | ||
} | ||
} | ||
|
||
pub struct Remove { | ||
pub pattern: String, | ||
} | ||
|
||
impl RenameCommand for Remove { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let new_name = old_name.to_string_lossy().replace(&self.pattern, ""); | ||
PathBuf::from(new_name) | ||
} | ||
} | ||
|
||
pub struct Replace { | ||
pub pattern: String, | ||
pub replacement: String, | ||
pub is_regex: bool, | ||
} | ||
|
||
impl RenameCommand for Replace { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let path_str = old_name.to_string_lossy().to_string(); | ||
let new_name = if self.is_regex { | ||
let re = Regex::new(&self.pattern).unwrap(); | ||
re.replace_all(&path_str, &self.replacement).to_string() | ||
} else { | ||
path_str.replace(&self.pattern, &self.replacement) | ||
}; | ||
PathBuf::from(new_name) | ||
} | ||
} | ||
|
||
pub struct ChangeCase { | ||
pub upper: bool, | ||
} | ||
|
||
impl RenameCommand for ChangeCase { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let path_str = old_name.to_string_lossy().to_string(); | ||
let new_name = match self.upper { | ||
true => path_str.to_uppercase(), | ||
false => path_str.to_lowercase(), | ||
}; | ||
PathBuf::from(new_name) | ||
} | ||
} | ||
|
||
pub struct FixExtension { | ||
pub append: bool, | ||
} | ||
|
||
impl RenameCommand for FixExtension { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let possible_extensions = find_extensions_from_content(old_name); | ||
let mut new_name = old_name.to_path_buf(); | ||
if !has_correct_extension(old_name, &possible_extensions) { | ||
let mut new_extension = possible_extensions[0].clone(); | ||
if self.append { | ||
if let Some(old_extension) = new_name.extension() { | ||
new_extension.insert(0, '.'); | ||
new_extension.insert_str(0, old_extension.to_str().unwrap()) | ||
} | ||
} | ||
new_name.set_extension(new_extension); | ||
}; | ||
new_name | ||
} | ||
} | ||
|
||
pub struct Prefix { | ||
pub prefix: String, | ||
} | ||
|
||
impl RenameCommand for Prefix { | ||
fn suggest_new_name(&self, old_name: &Path) -> PathBuf { | ||
let mut new_name = self.prefix.clone(); | ||
new_name.push_str(old_name.to_string_lossy().to_string().as_str()); | ||
PathBuf::from(new_name) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use std::path::PathBuf; | ||
|
||
/// Compare whether old_names are converted to new expected_names using command. | ||
fn assert_renames_correctly( | ||
command: &dyn RenameCommand, | ||
old_names: &[&str], | ||
expected_names: &[&str], | ||
) { | ||
let old: Vec<PathBuf> = old_names.iter().map(|&x| PathBuf::from(x)).collect(); | ||
let new_intents = command.suggest_renames(&old); | ||
let new: Vec<PathBuf> = new_intents | ||
.iter() | ||
.map(|intent| intent.new_name.clone()) | ||
.collect(); | ||
let expected: Vec<PathBuf> = expected_names.iter().map(|&x| PathBuf::from(x)).collect(); | ||
assert_eq!(expected, new); | ||
} | ||
|
||
#[test] | ||
fn test_prefix() { | ||
assert_renames_correctly( | ||
&Prefix { prefix: String::from("a") }, | ||
&["b", "a"], | ||
&["ab", "aa"] | ||
); | ||
} | ||
|
||
mod test_replace { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_regex() { | ||
let command = Replace { | ||
pattern: String::from("\\d"), | ||
replacement: String::from("a"), | ||
is_regex: true, | ||
}; | ||
assert_renames_correctly( | ||
&command, | ||
&["222", "abc", "answer_is_42", "\\d2"], | ||
&["aaa", "abc", "answer_is_aa", "\\da"] | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_non_regex() { | ||
let command = Replace { | ||
pattern: String::from("a.c"), | ||
replacement: String::from("def"), | ||
is_regex: false, | ||
}; | ||
assert_renames_correctly( | ||
&command, | ||
&["a.c", "abc", "ABC"], | ||
&["def", "abc", "ABC"] | ||
); | ||
} | ||
} | ||
|
||
mod test_change_case { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_upper() { | ||
assert_renames_correctly( | ||
&ChangeCase { upper: true }, | ||
&["Abc", "hnědý", "Αθήνα", "mountAIN🗻"], | ||
&["ABC", "HNĚDÝ", "ΑΘΉΝΑ", "MOUNTAIN🗻"] | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_lower() { | ||
assert_renames_correctly( | ||
&ChangeCase { upper: false }, | ||
&["Abc", "hnědý", "Αθήνα", "mountAIN🗻"], | ||
&["abc", "hnědý", "αθήνα", "mountain🗻"] | ||
); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_normalize() { | ||
assert_renames_correctly( | ||
&Normalize, | ||
&["Abc", "hnědý", "Αθήνα & Σπάρτη", "mountain🗻"], | ||
&["Abc", "hnedy", "Athena_&_Sparte", "mountain"] | ||
); | ||
} | ||
|
||
mod test_set_extension { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_no_extension() { | ||
assert_renames_correctly( | ||
&SetExtension{ extension: String::from("") }, | ||
&["a", "b", "c.jpg", ".gitignore"], | ||
&["a", "b", "c", ".gitignore"], | ||
); | ||
} | ||
|
||
#[test] | ||
fn test_some_extension() { | ||
assert_renames_correctly( | ||
&SetExtension{ extension: String::from("jpg") }, | ||
&["a", "b", "c.jpg", ".gitignore"], | ||
&["a.jpg", "b.jpg", "c.jpg", ".gitignore.jpg"], | ||
); | ||
} | ||
} | ||
|
||
#[test] | ||
fn test_remove() { | ||
assert_renames_correctly( | ||
&Remove{ pattern: String::from("ab") }, | ||
&[".gitignore", "babe", "abABab"], | ||
&[".gitignore", "be", "AB"] | ||
) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
use std::io::ErrorKind; | ||
use std::path::Path; | ||
use std::process::{self, exit}; | ||
|
||
fn infer_mimetype(path: &Path, mime_type: bool) -> Option<String> { | ||
// TODO: Do something on windows :see_no_evil: | ||
let mut cmd = process::Command::new("file"); | ||
let cmd_with_args = cmd.arg(path).arg("--brief"); | ||
let cmd_with_args = if mime_type { | ||
cmd_with_args.arg("--mime-type") | ||
} else { | ||
cmd_with_args | ||
}; | ||
|
||
let output = cmd_with_args.output(); | ||
match output { | ||
Ok(output) => { | ||
let output_str = String::from_utf8(output.stdout).unwrap(); | ||
let mime_type = match output_str.strip_suffix('\n') { | ||
Some(s) => String::from(s), | ||
None => output_str, | ||
}; | ||
Some(mime_type) | ||
} | ||
Err(e) => match e.kind() { | ||
ErrorKind::NotFound => { | ||
eprintln!("Error: `file` probably not installed"); | ||
exit(-1); | ||
} | ||
_ => panic!("{e}"), | ||
}, | ||
} | ||
} | ||
|
||
pub fn find_extensions_from_content(path: &Path) -> Vec<String> { | ||
let mime_type_based = match infer_mimetype(path, true) { | ||
None => vec![], | ||
Some(mime_type) => { | ||
let mime_type_str = mime_type.as_str(); | ||
match mime_type_str { | ||
"application/pdf" => vec![String::from("pdf")], | ||
"image/jpeg" => vec![String::from("jpeg"), String::from("jpg")], | ||
"image/png" => vec![String::from("png")], | ||
"text/csv" => vec![String::from("csv")], | ||
"text/html" => vec![String::from("html"), String::from("htm")], | ||
"text/x-script.python" => vec![String::from("py"), String::from("pyw")], | ||
_other => vec![], | ||
} | ||
} | ||
}; | ||
|
||
let mut description_based = match infer_mimetype(path, false) { | ||
None => vec![], | ||
Some(description) => { | ||
let description_str = description.as_str(); | ||
match description_str { | ||
"Apache Parquet" => vec![String::from("parquet"), String::from("pq")], | ||
_other => vec![], | ||
} | ||
} | ||
}; | ||
|
||
let mut extensions = mime_type_based.clone(); | ||
extensions.append(&mut description_based); | ||
extensions | ||
} | ||
|
||
pub fn has_correct_extension(path: &Path, possible_extensions: &[String]) -> bool { | ||
if possible_extensions.is_empty() { | ||
true | ||
} else { | ||
let current_extension = path.extension(); | ||
if current_extension.is_none() { | ||
false | ||
} else { | ||
let extension = current_extension.unwrap().to_ascii_lowercase(); | ||
let extension_str = String::from(extension.to_string_lossy()); | ||
possible_extensions.contains(&extension_str) | ||
} | ||
} | ||
} |
Oops, something went wrong.