├── src ├── lib.rs ├── mmv │ ├── file_utils │ │ ├── mod.rs │ │ ├── resolve_path_pattern.rs │ │ └── file_template.rs │ ├── rename_mod.rs │ ├── errors.rs │ ├── template_applier.rs │ └── mod.rs └── main.rs ├── .gitignore ├── tests ├── tests │ ├── templates_error │ │ ├── double_asterisk.json │ │ ├── asterisk_before_last_part.json │ │ └── captures_not_covered_by_flags.json │ ├── simple.json │ ├── directory.json │ ├── nothing_to_rename │ │ ├── simple.json │ │ └── png.json │ ├── terminate2.json │ ├── terminate.json │ ├── skip_when_exists.json │ ├── overwrite_when_exists.json │ ├── escaped_asterisk.json │ ├── output_template_with_hash.json │ ├── flag_before_last_part.json │ ├── png.json │ └── multiple_asterisks.json ├── tests.rs └── files_environment.rs ├── Cargo.toml ├── README.md └── LICENSE /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | mod mmv; 4 | 5 | pub use mmv::{ActionWhenRenamedFilePathExists, TemplateFileRenamer, TfrError}; 6 | -------------------------------------------------------------------------------- /src/mmv/file_utils/mod.rs: -------------------------------------------------------------------------------- 1 | mod file_template; 2 | mod resolve_path_pattern; 3 | 4 | pub use file_template::{Template, TemplateError}; 5 | pub use resolve_path_pattern::resolve_path_pattern; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build 2 | target/ 3 | /tools/target 4 | /Cargo.lock 5 | 6 | # VS Code settings 7 | **/.vscode 8 | 9 | # IDEA 10 | **/.idea 11 | 12 | # MacOS useless DS_Store 13 | **/.DS_Store 14 | -------------------------------------------------------------------------------- /tests/tests/templates_error/double_asterisk.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "double_asterisk", 3 | "input_template": "from**.txt", 4 | "output_template": "to#1#2.txt", 5 | "before": [], 6 | "after": [], 7 | "raise_error": true 8 | } -------------------------------------------------------------------------------- /src/mmv/rename_mod.rs: -------------------------------------------------------------------------------- 1 | /// Possible behavior of `TemplateFileRenamer` when a new file path already exists 2 | #[derive(Default, Eq, PartialEq)] 3 | pub enum ActionWhenRenamedFilePathExists { 4 | #[default] 5 | Terminate, 6 | Skip, 7 | Overwrite, 8 | } 9 | -------------------------------------------------------------------------------- /tests/tests/templates_error/asterisk_before_last_part.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "asterisk_before_last_part", 3 | "input_template": "path*/from.txt", 4 | "output_template": "path/to#1.txt", 5 | "before": [], 6 | "after": [], 7 | "raise_error": true 8 | } -------------------------------------------------------------------------------- /tests/tests/templates_error/captures_not_covered_by_flags.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "captures_not_covered_by_flags", 3 | "input_template": "path/one_*_two_*_three_*.txt", 4 | "output_template": "path/one_#1_three_#3.txt", 5 | "before": [], 6 | "after": [], 7 | "raise_error": true 8 | } -------------------------------------------------------------------------------- /tests/tests/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "simple", 3 | "input_template": "before.txt", 4 | "output_template": "after.txt", 5 | "before": [ 6 | ["before.txt", "after.txt"], 7 | ["other.txt", null] 8 | ], 9 | "after": [ 10 | "after.txt", 11 | "other.txt" 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/tests/directory.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "directory", 3 | "input_template": "*directory", 4 | "output_template": "directory#1", 5 | "before": [ 6 | ["1directory/", null], 7 | ["2directory", "directory2"] 8 | ], 9 | "after": [ 10 | "1directory/", 11 | "directory2" 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/tests/nothing_to_rename/simple.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "nothing_to_rename_simple", 3 | "input_template": "before", 4 | "output_template": "after.txt", 5 | "before": [ 6 | ["before.txt", null], 7 | ["other.txt", null] 8 | ], 9 | "after": [ 10 | "before.txt", 11 | "other.txt" 12 | ] 13 | } -------------------------------------------------------------------------------- /tests/tests/terminate2.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "terminate2", 3 | "input_template": "from.txt", 4 | "output_template": "to", 5 | "before": [ 6 | ["from.txt", null], 7 | ["to/", null] 8 | ], 9 | "after": [ 10 | "from.txt", 11 | "to/" 12 | ], 13 | "raise_error": true, 14 | "action_when_exists": "overwrite" 15 | } -------------------------------------------------------------------------------- /tests/tests/terminate.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "terminate", 3 | "input_template": "from.txt", 4 | "output_template": "to.txt", 5 | "before": [ 6 | ["from.txt", null], 7 | ["to.txt", null] 8 | ], 9 | "after": [ 10 | "from.txt", 11 | "to.txt" 12 | ], 13 | "raise_error": true, 14 | "action_when_exists": "terminate" 15 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tfr" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | clap = { version = "4.4.6", features = ["derive"] } 10 | regex = "1.10.0" 11 | chrono = "0.4.31" 12 | itertools = "0.11.0" 13 | serde = { version = "*", features = ["derive"] } 14 | serde_json = "*" 15 | 16 | -------------------------------------------------------------------------------- /tests/tests/skip_when_exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "skip_when_exists", 3 | "input_template": "before_*.txt", 4 | "output_template": "new/path/after_#1.txt", 5 | "before": [ 6 | ["before_1.txt", null], 7 | ["new/path/after_1.txt", null] 8 | ], 9 | "after": [ 10 | "before_1.txt", 11 | "new/path/after_1.txt" 12 | ], 13 | "raise_error": false, 14 | "action_when_exists": "skip" 15 | } -------------------------------------------------------------------------------- /tests/tests/overwrite_when_exists.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "overwrite_when_exists", 3 | "input_template": "before_*.txt", 4 | "output_template": "new/path/after_#1.txt", 5 | "before": [ 6 | ["before_1.txt", "new/path/after_1.txt"], 7 | ["new/path/after_1.txt", null] 8 | ], 9 | "after": [ 10 | "new/path/after_1.txt" 11 | ], 12 | "raise_error": false, 13 | "action_when_exists": "overwrite" 14 | } -------------------------------------------------------------------------------- /tests/tests/escaped_asterisk.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "escaped_asterisk", 3 | "input_template": "file\\*_*.*", 4 | "output_template": "move/to/file_*_#1.#2", 5 | "before": [ 6 | ["file*_baza.txt", "move/to/file_*_baza.txt"], 7 | ["file*_vaza.png", "move/to/file_*_vaza.png"], 8 | ["filename_#aza.mov", null] 9 | ], 10 | "after": [ 11 | "move/to/file_*_baza.txt", 12 | "move/to/file_*_vaza.png", 13 | "filename_#aza.mov" 14 | ] 15 | } -------------------------------------------------------------------------------- /tests/tests/output_template_with_hash.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "output_template_with_hash", 3 | "input_template": "file_*.txt", 4 | "output_template": "new_#_file_#1.txt", 5 | "before": [ 6 | ["file_1.txt", "new_#_file_1.txt"], 7 | ["file_lol.txt", "new_#_file_lol.txt"], 8 | ["file_.txt", "new_#_file_.txt"], 9 | ["file.txt", null] 10 | ], 11 | "after": [ 12 | "new_#_file_1.txt", 13 | "new_#_file_lol.txt", 14 | "new_#_file_.txt", 15 | "file.txt" 16 | ] 17 | } -------------------------------------------------------------------------------- /tests/tests/flag_before_last_part.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "flag_before_last_part", 3 | "input_template": "path/to/some_*_filename.*", 4 | "output_template": "path2/#1/file.#2", 5 | "before": [ 6 | ["path/to/some_A_filename.bin", "path2/A/file.bin"], 7 | ["path/to/some_A_filename.jpg", "path2/A/file.jpg"], 8 | ["path/to/some_B_filename.bin", "path2/B/file.bin"], 9 | ["path/to/some_B_filename.jpg", "path2/B/file.jpg"] 10 | ], 11 | "after": [ 12 | "path/to/", 13 | "path2/A/file.bin", 14 | "path2/A/file.jpg", 15 | "path2/B/file.bin", 16 | "path2/B/file.jpg" 17 | ] 18 | } -------------------------------------------------------------------------------- /tests/tests/nothing_to_rename/png.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "nothing_to_rename_png", 3 | "input_template": "path/to/*.png", 4 | "output_template": "path/to/images/#1.png", 5 | "before": [ 6 | ["path/to/image.jpg", null], 7 | ["path/to/other_image.jpg", null], 8 | ["path/to/not_image.txt", null], 9 | ["path/to/trash.cpp", null], 10 | ["path/to/other_trash.py", null], 11 | ["path/to/wtf.asm", null] 12 | ], 13 | "after": [ 14 | "path/to/image.jpg", 15 | "path/to/other_image.jpg", 16 | "path/to/not_image.txt", 17 | "path/to/trash.cpp", 18 | "path/to/other_trash.py", 19 | "path/to/wtf.asm" 20 | ] 21 | } -------------------------------------------------------------------------------- /tests/tests/png.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "png", 3 | "input_template": "path/to/*.png", 4 | "output_template": "path/to/images/#1.png", 5 | "before": [ 6 | ["path/to/image.png", "path/to/images/image.png"], 7 | ["path/to/other_image.png", "path/to/images/other_image.png"], 8 | ["path/to/not_image.txt", null], 9 | ["path/to/trash.cpp", null], 10 | ["path/to/other_trash.py", null], 11 | ["path/to/wtf.asm", null] 12 | ], 13 | "after": [ 14 | "path/to/images/image.png", 15 | "path/to/images/other_image.png", 16 | "path/to/not_image.txt", 17 | "path/to/trash.cpp", 18 | "path/to/other_trash.py", 19 | "path/to/wtf.asm" 20 | ] 21 | } -------------------------------------------------------------------------------- /tests/tests/multiple_asterisks.json: -------------------------------------------------------------------------------- 1 | { 2 | "environment_name": "multiple_asterisks", 3 | "input_template": "path/to/some_*_filename.*", 4 | "output_template": "path2/to/changed_#1_filename.#2", 5 | "before": [ 6 | ["path/to/some_A_filename.bin", "path2/to/changed_A_filename.bin"], 7 | ["path/to/some_A_filename.jpg", "path2/to/changed_A_filename.jpg"], 8 | ["path/to/some_B_filename.bin", "path2/to/changed_B_filename.bin"], 9 | ["path/to/some_B_filename.jpg", "path2/to/changed_B_filename.jpg"] 10 | ], 11 | "after": [ 12 | "path/to/", 13 | "path2/to/changed_A_filename.bin", 14 | "path2/to/changed_A_filename.jpg", 15 | "path2/to/changed_B_filename.bin", 16 | "path2/to/changed_B_filename.jpg" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/mmv/errors.rs: -------------------------------------------------------------------------------- 1 | use crate::mmv::file_utils::TemplateError; 2 | use crate::mmv::TfrError::IncorrectInputTemplate; 3 | 4 | /// Common Template File Renamer Errors 5 | /// 6 | /// - `IncorrectInputTemplate` and `IncorrectOutputTemplate` occur when the passed templates are incorrect. 7 | /// - `ExistingPath` occurs when the renaming mod is terminated if an existing path is found or existing path 8 | /// is something except file 9 | /// - Other errors ([std::error::Error](std::error::Error)) saved in 'StdError'. It was expected that only errors from 10 | /// [fs::rename](std::fs::rename) can be occur here. 11 | #[derive(Debug)] 12 | pub enum TfrError { 13 | IncorrectInputTemplate(&'static str), 14 | IncorrectOutputTemplate(&'static str), 15 | ExistingPath(/*path=*/ String, /*is_file=*/ bool), 16 | StdError(Box), 17 | } 18 | 19 | impl From for TfrError { 20 | fn from(template_err: TemplateError) -> Self { 21 | match template_err { 22 | TemplateError::AsteriskInDirectory => { 23 | IncorrectInputTemplate("Found asterisks in directory of input template") 24 | } 25 | TemplateError::DoubleAsterisk => { 26 | IncorrectInputTemplate("Found double asterisk in input template") 27 | } 28 | } 29 | } 30 | } 31 | 32 | impl From for TfrError 33 | where 34 | StdError: std::error::Error + 'static, 35 | { 36 | fn from(template_err: StdError) -> Self { 37 | Self::StdError(template_err.into()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mmv/file_utils/resolve_path_pattern.rs: -------------------------------------------------------------------------------- 1 | use regex::{Captures, Regex}; 2 | 3 | pub fn resolve_path_pattern(path_pattern: &str, captures: Vec<&str>) -> String { 4 | let placement_regex = Regex::new(r#"#(\d+)"#).unwrap(); 5 | 6 | placement_regex 7 | .replace_all(path_pattern, |capture: &Captures| { 8 | let index = capture.get(1).unwrap().as_str().parse::().unwrap(); 9 | if 1 <= index && index <= captures.len() { 10 | captures[index - 1] 11 | } else { 12 | path_pattern.get(capture.get(0).unwrap().range()).unwrap() 13 | } 14 | }) 15 | .into() 16 | } 17 | 18 | #[cfg(test)] 19 | mod tests { 20 | use super::*; 21 | 22 | #[test] 23 | fn simple_test() { 24 | assert_eq!(resolve_path_pattern("", vec![]), ""); 25 | assert_eq!(resolve_path_pattern("", vec!["capture"]), ""); 26 | assert_eq!(resolve_path_pattern("pattern", vec![]), "pattern"); 27 | assert_eq!(resolve_path_pattern("pattern", vec!["capture"]), "pattern"); 28 | assert_eq!(resolve_path_pattern("#1", vec!["capture"]), "capture"); 29 | assert_eq!(resolve_path_pattern("#1", vec![""]), ""); 30 | } 31 | 32 | #[test] 33 | fn multiple_usage_test() { 34 | assert_eq!( 35 | resolve_path_pattern("double #1 #1", vec!["capture"]), 36 | "double capture capture" 37 | ); 38 | assert_eq!( 39 | resolve_path_pattern("double #1 #2 #1", vec!["capture", "double"]), 40 | "double capture double capture" 41 | ); 42 | assert_eq!(resolve_path_pattern("#1#1", vec!["test"]), "testtest"); 43 | assert_eq!(resolve_path_pattern("#1#1", vec![""]), ""); 44 | } 45 | 46 | #[test] 47 | fn wrong_patterns_test() { 48 | assert_eq!(resolve_path_pattern("#0, #1, #2", vec!["ok"]), "#0, ok, #2"); 49 | assert_eq!(resolve_path_pattern("#0, #1", vec![]), "#0, #1"); 50 | assert_eq!(resolve_path_pattern("#0", vec![]), "#0"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🪢 TFR - Template file renamer 2 | 3 | A tool for fast renaming or moving files according to a given input and output templates. An analog to linux mmv 4 | 5 | Project on the HSE Rust course 6 | 7 | ## 🔧 Installation 8 | 9 | You must have the project [dependencies](#-dependencies) installed 10 | 11 | 1. Cloning a repository 12 | 13 | ```shell 14 | git clone https://github.com/X-OrBit/tfr 15 | ``` 16 | 17 | 2. Going to the tfr directory 18 | 19 | ```shell 20 | cd tfr 21 | ``` 22 | 23 | 3. Building 24 | 25 | ```shell 26 | cargo build -p tfr -r 27 | ``` 28 | 29 | The binary file will be located along the path `./target/release/tfr` 30 | 31 | ## 📦 Releases 32 | 33 | Releases and builds of the program can be found at the [link](https://github.com/X-OrBit/tfr/releases) 34 | 35 | ## 👔 Dependencies 36 | 37 | For this project you must have installed Rust compiler and cargo: 38 | 39 | ### MacOS 40 | 41 | #### Homebrew 42 | ```shell 43 | sudo brew install rust 44 | ``` 45 | 46 | #### MacPorts 47 | ```shell 48 | sudo port install rust 49 | ``` 50 | 51 | ### MacOS, Linux and other Unix-like OS 52 | 53 | Run the following in terminal, then follow the on-screen instructions: 54 | 55 | ```shell 56 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 57 | ``` 58 | 59 | ### Windows 60 | 61 | Download and run [rustup-init.exe](https://static.rust-lang.org/rustup/dist/i686-pc-windows-gnu/rustup-init.exe) 62 | 63 | 64 | ## 🚀 Usage 65 | 66 | Moving single file 67 | 68 | ```shell 69 | tfr source/file/path destination/file/path 70 | ``` 71 | 72 | Moving all files from directory 73 | 74 | ```shell 75 | tfr source/dir/path/* destination/file/path/#1 76 | ``` 77 | 78 | Moving only `.txt` files from directory 79 | ```shell 80 | tfr source/dir/path/*.txt destination/file/path/#1.txt 81 | ``` 82 | 83 | Change file name formats 84 | ```shell 85 | tfr source/dir/path/image_*_from_*.* destination/file/path/#2_#1_image.#3 86 | ``` 87 | 88 | ## ⚠️ Possible problems 89 | 90 | There may be problems on systems where the file system does not support `/` 91 | 92 | 93 | ## ☑️ TODO list 94 | - [ ] Support for capture flags in directories (`source/dir_*/path/*.png`) 95 | - [ ] Support for including `/` in captures with special capture flag: `**` (`source/**/path/**.png`) 96 | - [ ] Support for moving/renaming directories (`source/directory/path/to/move`) 97 | - [ ] Support for insertion flags in directories (`destination/dir_#1/path/#2.png`) -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod mmv; 2 | 3 | use mmv::{ActionWhenRenamedFilePathExists, TemplateFileRenamer}; 4 | 5 | use crate::mmv::TfrError; 6 | use chrono::offset::Local; 7 | use clap::Parser; 8 | 9 | #[derive(Parser, Debug)] 10 | #[command(author, version, about, long_about = None)] 11 | struct Args { 12 | /// Input file path template 13 | /// 14 | /// To capture, use the asterisks: '*'. 15 | /// Asterisks are allowed only on the last part of the path. Double asterisk are not allowed 16 | /// 17 | /// Example: example/input/template/path_*.* 18 | input_file_template: String, 19 | 20 | /// Output file path template. 21 | /// 22 | /// To insert capture, use flag #. Multiple use of the same flag is allowed. All captures must be covered with at least one flag 23 | /// 24 | /// Example: example/output/template/new_#1_path_#1.#2 25 | output_file_template: String, 26 | 27 | /// Use the force flag to overwrite the path to the output file, if it exists 28 | #[arg(short, long, action)] 29 | force: bool, 30 | } 31 | 32 | fn main() { 33 | let args = Args::parse(); 34 | 35 | let input_file_template = args.input_file_template; 36 | let output_file_template = args.output_file_template; 37 | 38 | let start_time = Local::now(); 39 | let callback_handler = 40 | |processed: usize, total: usize, old_filepath: Option<&str>, new_filepath: Option<&str>| { 41 | if processed == 0 { 42 | return match total { 43 | 0 => { 44 | println!("Files for pattern '{input_file_template}' not found"); 45 | std::process::exit(1); 46 | } 47 | _ => { 48 | println!( 49 | "Started with params: {} -> {}. Files to rename: {}", 50 | input_file_template, output_file_template, total 51 | ) 52 | } 53 | }; 54 | } 55 | 56 | println!("{} -> {}", old_filepath.unwrap(), new_filepath.unwrap()); 57 | 58 | if processed == total { 59 | println!( 60 | "Finished in {}ms.", 61 | (Local::now() - start_time).num_milliseconds() 62 | ) 63 | } 64 | }; 65 | let mut tfr = TemplateFileRenamer::new(ActionWhenRenamedFilePathExists::Terminate); 66 | tfr.set_callback_handler(callback_handler); 67 | 68 | if let Err(tfr_error) = tfr.rename(&input_file_template, &output_file_template) { 69 | match tfr_error { 70 | TfrError::IncorrectInputTemplate(description) => { 71 | eprintln!("IncorrectInputTemplate error occurred: {description}") 72 | } 73 | TfrError::IncorrectOutputTemplate(description) => { 74 | eprintln!("IncorrectOutputTemplate error occurred: {description}") 75 | } 76 | TfrError::ExistingPath(existing_filepath, is_file) => { 77 | eprintln!( 78 | "Not able to replace existing {}: {}", 79 | if is_file { "file" } else { "path" }, 80 | existing_filepath 81 | ) 82 | } 83 | TfrError::StdError(error) => { 84 | eprintln!("Some error occurred: {:?}", error.as_ref()) 85 | } 86 | } 87 | std::process::exit(1); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | mod files_environment; 2 | 3 | use crate::files_environment::{FilesEnvironment, FilesEnvironmentConfig}; 4 | use tfr::TemplateFileRenamer; 5 | use std::io; 6 | use std::io::Read; 7 | 8 | mod integration_tests { 9 | use super::*; 10 | 11 | fn read_environment_config(config_path: &str) -> Result { 12 | let mut file = std::fs::File::open(config_path)?; 13 | let mut data = String::new(); 14 | file.read_to_string(&mut data)?; 15 | Ok(serde_json::from_str::(&data) 16 | .expect("JSON was not well-formatted")) 17 | } 18 | 19 | fn test_with_json_config(config_path: &str) { 20 | let config_path = format!("tests/tests/{}", config_path); 21 | let environment_config = read_environment_config(&config_path).unwrap(); 22 | let files_environment = FilesEnvironment::new(&environment_config).unwrap(); 23 | 24 | let tfr = TemplateFileRenamer::new(environment_config.action_when_exists.clone().into()); 25 | 26 | let result = tfr.rename( 27 | &files_environment.get_full_path(&environment_config.input_template), 28 | &files_environment.get_full_path(&environment_config.output_template), 29 | ); 30 | let is_correct_status = match result { 31 | Err(_) => environment_config.raise_error, 32 | Ok(_) => !environment_config.raise_error, 33 | }; 34 | 35 | assert!(is_correct_status && files_environment.is_after()) 36 | } 37 | 38 | #[test] 39 | fn simple_test() { 40 | test_with_json_config("simple.json"); 41 | } 42 | 43 | #[test] 44 | fn rename_test() { 45 | test_with_json_config("png.json"); 46 | } 47 | 48 | #[test] 49 | fn multiple_asterisks_test() { 50 | test_with_json_config("multiple_asterisks.json"); 51 | } 52 | 53 | #[test] 54 | fn flag_before_last_part_test() { 55 | test_with_json_config("flag_before_last_part.json"); 56 | } 57 | 58 | #[test] 59 | fn escaped_asterisk_test() { 60 | test_with_json_config("escaped_asterisk.json"); 61 | } 62 | 63 | #[test] 64 | fn directory_test() { 65 | test_with_json_config("directory.json"); 66 | } 67 | 68 | #[test] 69 | fn output_template_with_hash_test() { 70 | test_with_json_config("output_template_with_hash.json"); 71 | } 72 | 73 | #[test] 74 | fn nothing_to_rename_test() { 75 | test_with_json_config("nothing_to_rename/simple.json"); 76 | test_with_json_config("nothing_to_rename/png.json"); 77 | } 78 | 79 | #[test] 80 | fn terminate_test() { 81 | test_with_json_config("terminate.json"); 82 | } 83 | 84 | #[test] 85 | fn terminate_existing_dir_test() { 86 | test_with_json_config("terminate2.json"); 87 | } 88 | 89 | #[test] 90 | fn templates_error_test() { 91 | test_with_json_config("templates_error/asterisk_before_last_part.json"); 92 | test_with_json_config("templates_error/double_asterisk.json"); 93 | test_with_json_config("templates_error/captures_not_covered_by_flags.json"); 94 | } 95 | 96 | #[test] 97 | fn skip_when_exists_test() { 98 | test_with_json_config("skip_when_exists.json"); 99 | } 100 | 101 | #[test] 102 | fn overwrite_when_exists_test() { 103 | test_with_json_config("overwrite_when_exists.json"); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/mmv/file_utils/file_template.rs: -------------------------------------------------------------------------------- 1 | use regex::{escape, Captures, Regex}; 2 | 3 | #[derive(Debug, PartialEq, Eq)] 4 | pub enum TemplateError { 5 | AsteriskInDirectory, 6 | DoubleAsterisk, 7 | } 8 | 9 | pub struct Template { 10 | pattern: Regex, 11 | } 12 | 13 | impl Template { 14 | pub fn new(pattern: &str) -> Result { 15 | if pattern.rfind('/').unwrap_or(0) > pattern.find('*').unwrap_or(pattern.len()) { 16 | return Err(TemplateError::AsteriskInDirectory); 17 | } 18 | if pattern.contains("**") { 19 | return Err(TemplateError::DoubleAsterisk); 20 | } 21 | 22 | let escaped_pattern = escape(&String::from(pattern)); 23 | let pattern = format!( 24 | "^{}$", 25 | Regex::new(r#"(^|[^\\])\\\*"#) 26 | .unwrap() 27 | .replace_all(&escaped_pattern, |caps: &Captures| format!( 28 | "{}([^/]*)", 29 | &caps[1] 30 | ),) 31 | .replace(r#"\\\*"#, r#"\*"#) 32 | ); 33 | 34 | Ok(Self { 35 | pattern: Regex::new(&pattern).unwrap(), 36 | }) 37 | } 38 | 39 | pub fn captures<'a>(&self, string: &'a str) -> Option> { 40 | match self.pattern.captures(string) { 41 | None => None, 42 | Some(captures) => Some( 43 | captures 44 | .iter() 45 | .skip(1) 46 | .map(|capture| string.get(capture.unwrap().range()).unwrap()) 47 | .collect(), 48 | ), 49 | } 50 | } 51 | } 52 | 53 | #[cfg(test)] 54 | mod tests { 55 | use super::*; 56 | 57 | #[test] 58 | fn test_simple() { 59 | let template = Template::new("word").unwrap(); 60 | assert_eq!(template.captures("word"), Some(vec![])); 61 | assert_eq!(template.captures("not_word"), None); 62 | assert_eq!(template.captures("word_not"), None); 63 | } 64 | 65 | #[test] 66 | fn test_incorrect_template() { 67 | assert_eq!( 68 | Template::new("/path/to/*/*.png").err().unwrap(), 69 | TemplateError::AsteriskInDirectory 70 | ); 71 | assert_eq!( 72 | Template::new("/path/to/**.png").err().unwrap(), 73 | TemplateError::DoubleAsterisk 74 | ); 75 | } 76 | 77 | #[test] 78 | fn escaped_asterisk() { 79 | let template = Template::new(r#"asterisk\*asterisk"#).unwrap(); 80 | assert_eq!(template.captures("asterisk*asterisk"), Some(vec![])); 81 | assert_eq!(template.captures("asterisk_asterisk"), None); 82 | } 83 | 84 | #[test] 85 | fn test_correct_templates() { 86 | assert_eq!( 87 | Template::new("/path/to/*.png") 88 | .unwrap() 89 | .captures("/path/to/image.png"), 90 | Some(vec!["image"]) 91 | ); 92 | 93 | let template = Template::new("path/to/some_*_filename.*").unwrap(); 94 | assert_eq!( 95 | template.captures("path/to/some_A_filename.bin"), 96 | Some(vec!["A", "bin"]) 97 | ); 98 | assert_eq!( 99 | template.captures("path/to/some_A_filename.jpg"), 100 | Some(vec!["A", "jpg"]) 101 | ); 102 | assert_eq!( 103 | template.captures("path/to/some_B_filename.bin"), 104 | Some(vec!["B", "bin"]) 105 | ); 106 | assert_eq!( 107 | template.captures("path/to/some_B_filename.jpg"), 108 | Some(vec!["B", "jpg"]) 109 | ); 110 | assert_eq!( 111 | template.captures("path/to/some__filename.jpg"), 112 | Some(vec!["", "jpg"]) 113 | ); 114 | assert_eq!(template.captures("path/to/some_filename.jpg"), None); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/mmv/template_applier.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | use regex::Regex; 3 | use std::fs::DirEntry; 4 | use std::path::{Path, PathBuf}; 5 | use std::{fs, io}; 6 | 7 | use crate::mmv::file_utils::{resolve_path_pattern, Template}; 8 | use crate::mmv::{ActionWhenRenamedFilePathExists, TfrError}; 9 | 10 | pub fn is_rename_template_correct(input_file_template: &str, output_file_template: &str) -> bool { 11 | let asterisk_count = 12 | input_file_template.matches('*').count() - input_file_template.matches("\\*").count(); 13 | let placement_regex = Regex::new(r#"#(\d+)"#).unwrap(); 14 | let correct_unique_flag_count: usize = placement_regex 15 | .captures_iter(output_file_template) 16 | .filter_map(|capture| { 17 | let index = capture.get(1).unwrap().as_str().parse::().unwrap(); 18 | if 1 <= index && index <= asterisk_count { 19 | Some(index) 20 | } else { 21 | None 22 | } 23 | }) 24 | .unique() 25 | .count(); 26 | 27 | correct_unique_flag_count >= asterisk_count 28 | } 29 | 30 | pub fn apply_template( 31 | input_file_template: &str, 32 | output_file_template: &str, 33 | rename_mod: &ActionWhenRenamedFilePathExists, 34 | ) -> Result, TfrError> { 35 | if !is_rename_template_correct(input_file_template, output_file_template) { 36 | return Err(TfrError::IncorrectOutputTemplate( 37 | "Output template flags does not cover input template asterisks", 38 | )); 39 | } 40 | 41 | let input_dir = 42 | Path::new(input_file_template) 43 | .parent() 44 | .ok_or(TfrError::IncorrectInputTemplate( 45 | "Empty input template does not allowed", 46 | ))?; 47 | let input_dir = fs::read_dir(input_dir).map_err(|_| { 48 | TfrError::IncorrectInputTemplate("Input template parent directory not found") 49 | })?; 50 | 51 | let input_file_template = Template::new(input_file_template)?; 52 | 53 | let extract_file_path = |path: DirEntry| { 54 | if path.file_type().unwrap().is_file() { 55 | return Some(path.path()); 56 | } 57 | None 58 | }; 59 | let file_candidates = input_dir 60 | .filter_map(|entry: io::Result| entry.ok().and_then(extract_file_path)) 61 | .collect::>(); 62 | 63 | let mut existing_path: Option = None; 64 | 65 | let mut apply_template_to_filepath = 66 | |input_path: &str, captures: Vec<&str>| -> Option<(String, String)> { 67 | let new_filepath = resolve_path_pattern(output_file_template, captures); 68 | if !Path::new(&new_filepath).exists() { 69 | return Some((input_path.to_string(), new_filepath)); 70 | } 71 | if Path::new(&new_filepath).is_dir() { 72 | existing_path = Some(new_filepath.to_string()); 73 | return None; 74 | } 75 | match rename_mod { 76 | ActionWhenRenamedFilePathExists::Terminate => { 77 | existing_path = Some(new_filepath.to_string()); 78 | None 79 | } 80 | ActionWhenRenamedFilePathExists::Skip => None, 81 | ActionWhenRenamedFilePathExists::Overwrite => { 82 | Some((input_path.to_string(), new_filepath)) 83 | } 84 | } 85 | }; 86 | 87 | let applied_new_filepaths: Vec<(String, String)> = file_candidates 88 | .iter() 89 | .filter_map(|input_path: &PathBuf| { 90 | let input_path = input_path.to_str().unwrap().to_string(); 91 | input_file_template 92 | .captures(&input_path) 93 | .and_then(|captures| apply_template_to_filepath(&input_path, captures)) 94 | }) 95 | .collect(); 96 | 97 | match existing_path { 98 | None => Ok(applied_new_filepaths), 99 | Some(existing_path) => Err(TfrError::ExistingPath( 100 | existing_path.clone(), 101 | Path::new(&existing_path).is_file(), 102 | )), 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/mmv/mod.rs: -------------------------------------------------------------------------------- 1 | mod errors; 2 | mod file_utils; 3 | mod rename_mod; 4 | mod template_applier; 5 | 6 | pub use errors::TfrError; 7 | pub use rename_mod::ActionWhenRenamedFilePathExists; 8 | 9 | use std::fs; 10 | use std::fs::create_dir_all; 11 | use std::path::Path; 12 | 13 | use crate::ActionWhenRenamedFilePathExists::Overwrite; 14 | use template_applier::apply_template; 15 | 16 | type CallbackHandler<'ch> = dyn Fn(usize, usize, Option<&str>, Option<&str>) + 'ch; 17 | 18 | /// Provides template file paths renaming. 19 | /// 20 | /// It is possible to use one of the 3 renaming mods [ActionWhenRenamedFilePathExists](ActionWhenRenamedFilePathExists) 21 | /// 22 | /// While `TemplateFileRenamer` renames matched files, a custom 23 | /// [CallbackHandler](TemplateFileRenamer::set_callback_handler) is called 24 | /// 25 | /// # Examples 26 | /// ``` 27 | /// use tfr::{ActionWhenRenamedFilePathExists, TemplateFileRenamer, TfrError}; 28 | /// let tfr = TemplateFileRenamer::new(ActionWhenRenamedFilePathExists::Terminate); 29 | /// let _ = tfr.rename("path/to/before_*.*", "path/to/after_#1.#2"); 30 | /// ``` 31 | #[derive(Default)] 32 | pub struct TemplateFileRenamer<'ch> { 33 | rename_mod: ActionWhenRenamedFilePathExists, 34 | callback_handler: Option>>, 35 | } 36 | 37 | impl<'ch> TemplateFileRenamer<'ch> { 38 | pub fn new(rename_mod: ActionWhenRenamedFilePathExists) -> Self { 39 | Self { 40 | rename_mod, 41 | callback_handler: None, 42 | } 43 | } 44 | 45 | /// # Signature 46 | /// dyn Fn(processed: usize, total: usize, old_filepath: Option<&str>, new_filepath: Option<&str>) 47 | /// 48 | /// # Description 49 | /// [TemplateFileRenamer](TemplateFileRenamer) provides custom user defined renaming callback handler 50 | /// 51 | /// When renaming is just started, `TFR` call `callback_handler` with arguments `processed=0` and `total` equal to count 52 | /// of files that matched `input_file_template`. `old_filepath` and `new_filepath` is None. 53 | /// 54 | /// When `processed` file was renamed, `TFR` call `callback_handler` with corresponding arguments. 55 | /// 56 | /// # Example 57 | /// ``` 58 | /// let callback_handler = |processed: usize, total: usize, old_filepath: Option<&str>, new_filepath: Option<&str>| { 59 | /// if processed == 0 { 60 | /// println!("Renaming started") 61 | /// } else { 62 | /// println!("{} of {} already renamed", processed, total) 63 | /// }; 64 | /// }; 65 | /// ``` 66 | pub fn set_callback_handler( 67 | &mut self, 68 | callback_handler: impl Fn(usize, usize, Option<&str>, Option<&str>) + 'ch, 69 | ) { 70 | self.callback_handler = Some(Box::new(callback_handler)) 71 | } 72 | 73 | fn start(&self, total: usize) { 74 | if self.callback_handler.is_some() { 75 | (self.callback_handler.as_ref().unwrap())(0, total, None, None) 76 | } 77 | } 78 | 79 | fn callback(&self, current: usize, total: usize, old_filepath: &str, new_filepath: &str) { 80 | if self.callback_handler.is_some() { 81 | (self.callback_handler.as_ref().unwrap())( 82 | current, 83 | total, 84 | Some(old_filepath), 85 | Some(new_filepath), 86 | ) 87 | } 88 | } 89 | 90 | /// Returns Ok(()) if all files matching the template have been successfully renamed 91 | /// 92 | /// Returns Err([TfrError](TfrError)) if any error occurred during renaming 93 | pub fn rename( 94 | &self, 95 | input_file_template: &str, 96 | output_file_template: &str, 97 | ) -> Result<(), TfrError> { 98 | let applied_new_filepaths = 99 | apply_template(input_file_template, output_file_template, &self.rename_mod)?; 100 | 101 | self.start(applied_new_filepaths.len()); 102 | 103 | for (idx, (first, second)) in applied_new_filepaths.iter().enumerate() { 104 | if let Some(parent) = Path::new(second).parent() { 105 | create_dir_all(parent)?; 106 | } 107 | 108 | if self.rename_mod == Overwrite && Path::new(second).is_file() { 109 | fs::remove_file(second)?; 110 | } 111 | fs::rename(first, second)?; 112 | self.callback(idx + 1, applied_new_filepaths.len(), first, second); 113 | } 114 | Ok(()) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /tests/files_environment.rs: -------------------------------------------------------------------------------- 1 | use tfr::ActionWhenRenamedFilePathExists; 2 | use serde::Deserialize; 3 | use std::collections::BTreeSet; 4 | use std::fs::{create_dir_all, read_to_string, remove_dir_all}; 5 | use std::path::Path; 6 | use std::{env, fs, io}; 7 | 8 | #[derive(Default, Deserialize, Debug, Clone)] 9 | #[serde(rename_all = "snake_case")] 10 | pub enum ActionWhenExists { 11 | #[default] 12 | Terminate, 13 | Skip, 14 | Overwrite, 15 | } 16 | 17 | impl Into for ActionWhenExists { 18 | fn into(self) -> ActionWhenRenamedFilePathExists { 19 | match self { 20 | ActionWhenExists::Terminate => ActionWhenRenamedFilePathExists::Terminate, 21 | ActionWhenExists::Skip => ActionWhenRenamedFilePathExists::Skip, 22 | ActionWhenExists::Overwrite => ActionWhenRenamedFilePathExists::Overwrite, 23 | } 24 | } 25 | } 26 | 27 | #[derive(Deserialize, Debug)] 28 | pub struct FilesEnvironmentConfig { 29 | pub environment_name: String, 30 | 31 | pub input_template: String, 32 | pub output_template: String, 33 | pub before: Vec<(String, Option)>, 34 | pub after: Vec, 35 | 36 | #[serde(default)] 37 | pub raise_error: bool, 38 | #[serde(default)] 39 | pub action_when_exists: ActionWhenExists, 40 | } 41 | 42 | #[derive(Debug)] 43 | pub struct FilesEnvironment<'a> { 44 | root: String, 45 | files_environment_config: &'a FilesEnvironmentConfig, 46 | } 47 | 48 | impl<'a> FilesEnvironment<'a> { 49 | pub fn get_full_path(&self, path: &str) -> String { 50 | format!("{}/{}", &self.root, path) 51 | } 52 | 53 | pub fn new(files_environment_config: &'a FilesEnvironmentConfig) -> Result { 54 | let root = env::temp_dir() 55 | .join("tfr-test-environment") 56 | .join(&files_environment_config.environment_name); 57 | create_dir_all(&root)?; 58 | 59 | for (path, _) in &files_environment_config.before { 60 | assert!(!path.is_empty()); 61 | let path = format!("{}/{}", root.to_str().unwrap(), path); 62 | match path.chars().last().unwrap() { 63 | '/' => { 64 | create_dir_all(path)?; 65 | } 66 | _ => { 67 | create_dir_all(Path::new(&path).parent().unwrap())?; 68 | fs::write(&path, &path)?; 69 | } 70 | } 71 | } 72 | Ok(Self { 73 | root: root.to_str().unwrap().to_string(), 74 | files_environment_config, 75 | }) 76 | } 77 | 78 | pub fn is_after(&self) -> bool { 79 | for after_path in &self.files_environment_config.after { 80 | let after_path = self.get_full_path(after_path); 81 | 82 | let is_correct = match after_path.chars().last().unwrap() { 83 | '/' => Path::new(&after_path).is_dir(), 84 | _ => Path::new(&after_path).is_file(), 85 | }; 86 | if !is_correct { 87 | return false; 88 | } 89 | } 90 | 91 | let all_renamed: BTreeSet<&String> = match self.files_environment_config.action_when_exists 92 | { 93 | ActionWhenExists::Overwrite => BTreeSet::from_iter( 94 | self.files_environment_config 95 | .before 96 | .iter() 97 | .filter_map(|(_before, after)| after.as_ref()), 98 | ), 99 | _ => BTreeSet::default(), 100 | }; 101 | 102 | for (before, after) in &self.files_environment_config.before { 103 | let full_before = self.get_full_path(before); 104 | 105 | let is_correct = match after { 106 | None => { 107 | // must not be moved 108 | if all_renamed.contains(&before) { 109 | continue; 110 | } 111 | if !Path::new(&full_before).exists() { 112 | false 113 | } else if before.chars().last().unwrap() != '/' { 114 | // is not directory 115 | read_to_string(&full_before).unwrap_or("".to_string()) == full_before 116 | } else { 117 | true 118 | } 119 | } 120 | Some(after) => { 121 | // must be moved 122 | let full_after = self.get_full_path(after); 123 | !Path::new(&full_before).exists() 124 | && read_to_string(&full_after).unwrap_or("".to_string()) == full_before 125 | } 126 | }; 127 | if !is_correct { 128 | return false; 129 | } 130 | } 131 | 132 | true 133 | } 134 | } 135 | 136 | impl<'a> Drop for FilesEnvironment<'a> { 137 | fn drop(&mut self) { 138 | remove_dir_all(&self.root).unwrap(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------