├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-MIT ├── README.md └── src ├── builder.rs ├── filter.rs ├── lib.rs ├── search.rs └── utils.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | 8 | env: 9 | CARGO_TERM_COLOR: always 10 | 11 | jobs: 12 | build-test: 13 | strategy: 14 | fail-fast: false 15 | 16 | matrix: 17 | include: 18 | - os: windows-latest 19 | target: x86_64-pc-windows-msvc 20 | 21 | - os: ubuntu-latest 22 | target: x86_64-unknown-linux-gnu 23 | 24 | - os: macos-latest 25 | target: x86_64-apple-darwin 26 | 27 | name: Build & Test (${{ matrix.target }}) 28 | runs-on: ${{ matrix.os }} 29 | 30 | env: 31 | RA_TARGET: ${{ matrix.target }} 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v3 36 | 37 | - name: Install Rust toolchain 38 | uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: stable 41 | target: ${{ matrix.target }} 42 | profile: minimal 43 | override: true 44 | 45 | - name: Install Rust library source 46 | if: matrix.target == 'x86_64-unknown-linux-gnu' 47 | uses: actions-rs/toolchain@v1 48 | with: 49 | toolchain: stable 50 | target: ${{ matrix.target }} 51 | profile: minimal 52 | override: true 53 | components: rust-src 54 | 55 | - name: Build 56 | run: cargo build --verbose --target ${{ matrix.target }} 57 | 58 | - name: Run tests 59 | run: cargo test --verbose --target ${{ matrix.target }} 60 | 61 | lint: 62 | name: Formatter 63 | 64 | needs: build-test 65 | 66 | runs-on: ubuntu-latest 67 | 68 | steps: 69 | - name: Checkout repository 70 | uses: actions/checkout@v3 71 | 72 | - name: Install Rust 73 | run: | 74 | rustup update stable 75 | rustup default stable 76 | rustup component add rustfmt 77 | rustup component add clippy 78 | 79 | - name: Check formatting 80 | run: cargo fmt --all -- --check 81 | 82 | - name: Check code for possible improvements 83 | run: cargo clippy -- -D warnings 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | 13 | # MSVC Windows builds of rustc generate these, which store debugging information 14 | *.pdb 15 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust_search" 3 | version = "2.1.0" 4 | description = "Blazingly fast file search library built in Rust" 5 | edition = "2021" 6 | authors = ["Parth Jadhav "] 7 | license = "MIT" 8 | readme = "README.md" 9 | repository = "https://github.com/ParthJadhav/rust_search" 10 | keywords = ["search", "files", "directories", "library"] 11 | categories = ["filesystem", "algorithms"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | regex = "1" 17 | ignore = "0.4" 18 | num_cpus = "1.0" 19 | dirs = "4.0.0" 20 | strsim = "0.10.0" -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Parth Jadhav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![Group 1](https://user-images.githubusercontent.com/42001064/198829818-c4035432-8721-45e1-ba2d-4d2eb6d0c584.svg) 4 | 5 | Blazingly fast file search crate built in Rust 🔥 6 | 7 | [![Version info](https://img.shields.io/crates/v/rust_search.svg)](https://crates.io/crates/rust_search) 8 | [![Documentation](https://docs.rs/rust_search/badge.svg)](https://docs.rs/rust_search) 9 | [![License](https://img.shields.io/crates/l/rust_search.svg)](https://github.com/parthjadhav/rust_search/blob/master/LICENSE-MIT) 10 | 11 |
12 | 13 | ## 📦 Usage 14 | 15 | Please report any problems you encounter when using rust search here: [Issues](https://github.com/ParthJadhav/rust_search/issues) 16 | 17 | Add `rust_search = "2.0.0"` in Cargo.toml. 18 | 19 | ```toml 20 | [dependencies] 21 | rust_search = "2.0.0" 22 | ``` 23 | 24 | ## Examples 25 | 26 | - General use 27 | 28 | ```rust 29 | use rust_search::SearchBuilder; 30 | 31 | fn main(){ 32 | let search: Vec = SearchBuilder::default() 33 | .location("~/path/to/directory") 34 | .search_input("what to search") 35 | .more_locations(vec!["/anotherPath/to/search", "/keepAddingIfYouWant/"]) 36 | .limit(1000) // results to return 37 | .ext("extension") 38 | .strict() 39 | .depth(1) 40 | .ignore_case() 41 | .hidden() 42 | .build() 43 | .collect(); 44 | 45 | for path in search { 46 | println!("{}", path); 47 | } 48 | } 49 | ``` 50 | 51 | - Sort the output by similarity with the input 52 | 53 | ```rust 54 | use rust_search::{SearchBuilder, similarity_sort}; 55 | fn main() { 56 | let search_input = "fly"; 57 | let mut search: Vec = SearchBuilder::default() 58 | .location("~/Desktop/") 59 | .search_input(search_input) 60 | .depth(1) 61 | .ignore_case() 62 | .build() 63 | .collect(); 64 | 65 | similarity_sort(&mut search, &search_input); 66 | for path in search { 67 | println!("{:?}", path); 68 | } 69 | } 70 | 71 | ``` 72 | > search **without** similarity sort 73 | `["afly.txt", "bfly.txt", "flyer.txt", "fly.txt"]` 74 | 75 | > search **with** similarity sort 76 | `["fly.txt", "flyer.txt", "afly.txt", "bfly.txt",]` 77 | 78 | - To get all the files with a specific extension in a directory, use: 79 | 80 | ```rust 81 | use rust_search::SearchBuilder; 82 | 83 | let files: Vec = SearchBuilder::default() 84 | .location("/path/to/directory") 85 | .ext("file_extension") 86 | .build() 87 | .collect(); 88 | ``` 89 | 90 | - To get all the files in a directory, use: 91 | 92 | ```rust 93 | use rust_search::SearchBuilder; 94 | 95 | let files: Vec = SearchBuilder::default() 96 | .location("/path/to/directory") 97 | .depth(1) 98 | .build() 99 | .collect(); 100 | ``` 101 | To filter files by date_created, date_modified, file_size and/or custom_filter, use: 102 | 103 | ```rust 104 | use rust_search::{FileSize, FilterExt, SearchBuilder}; 105 | use std::time::{Duration, SystemTime}; 106 | 107 | let search: Vec = SearchBuilder::default() 108 | .location("~/path/to/directory") 109 | .file_size_greater(FileSize::Kilobyte(200.0)) 110 | .file_size_smaller(FileSize::Megabyte(10.0)) 111 | .created_after(SystemTime::now() - Duration::from_secs(3600 * 24 * 10)) 112 | .created_before(SystemTime::now()) 113 | .modified_after(SystemTime::now() - Duration::from_secs(3600 * 24 * 5)) 114 | .custom_filter(|dir| dir.metadata().unwrap().is_file()) 115 | .custom_filter(|dir| !dir.metadata().unwrap().permissions().readonly()) 116 | .build() 117 | .collect(); 118 | ``` 119 | 120 | 👉 For more examples, please refer to the [Documentation](https://docs.rs/rust_search/latest/rust_search/) 121 | 122 | ## ⚙️ Benchmarks 123 | 124 | The difference in sample size is due to the fact that fd and glob are different tools and have different use cases. fd is a command line tool that searches for files and directories. glob is a library that can be used to search for files and directories. The benchmark is done on a MacBook Air M2, 16 GB Unified memory. 125 | 126 | Benchmarks are done using [hyperfine](https://github.com/sharkdp/hyperfine), 127 | Benchmarks files are available in the [benchmarks](https://drive.google.com/drive/folders/1ug6ojNixS5jAe6Lh6M0o2d3tku73zQ9w?usp=sharing) drive folder. 128 | 129 | ### - Rust Search vs Glob 130 | 131 | The benchmark was done on a directories containing 300K files. 132 | 133 | | Command / Library | Mean [s] | Min [s] | Max [s] | Relative | 134 | |:---|---:|---:|---:|---:| 135 | | `rust_search` | 1.317 ± 0.002 | 1.314 | 1.320 | 1.00 | 136 | | `glob` | 22.728 ± 0.023 | 22.690 | 22.746 | 17.25 ± 0.03 | 137 | 138 | --- 139 | 140 | ### - Rust Search vs FD 141 | 142 | The benchmark was done on a directories containing 45K files. 143 | 144 | | Command / Library | Mean [ms] | Min [ms] | Max [ms] | Relative | 145 | |:---|---:|---:|---:|---:| 146 | | `rust_search` | 680.5 ± 2.1 | 678.3 | 683.6 | 1.00 | 147 | | `fd -e .js` | 738.7 ± 10.2 | 720.8 | 746.7 | 1.09 ± 0.02 | 148 | 149 | --- 150 | 151 | ### Results:- 152 | 153 | ```diff 154 | + rust_search is 17.25 times faster than Glob. 155 | 156 | + rust_search** is 1.09 times faster than FD. 157 | ``` 158 | 159 | ## 👨‍💻 Contributors 160 | 161 | Any contributions would be greatly valued as this library is still in its early stages. 162 | 163 | - Documentation 164 | - Benchmarks 165 | - Implementation guidelines 166 | - Code Improvement 167 | 168 | If you want to contribute to this project, please follow the steps below: 169 | 170 | 1. Fork the project 171 | 2. Clone the forked repository 172 | 3. Create a feature branch 173 | 4. Make changes to the code 174 | 5. Commit the changes 175 | 6. Push the changes to the forked repository 176 | 7. Create a pull request 177 | 8. Wait for the pull request to be reviewed and merged (if approved) 178 | 179 | ## License 180 | 181 | This project is licensed under the terms of the MIT license. 182 | 183 | ## Discord server & Linkedin 184 | 185 | Click the button below to join the discord server or Linkedin 186 | 187 | Join Discord Server 188 | Connect on Linkedin 189 | -------------------------------------------------------------------------------- /src/builder.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use crate::filter::FilterType; 4 | use crate::{utils::replace_tilde_with_home_dir, Search}; 5 | 6 | /// Builder for a [`Search`] instance, allowing for more complex searches. 7 | pub struct SearchBuilder { 8 | /// The location to search in, defaults to the current directory. 9 | search_location: PathBuf, 10 | /// Additional locations to search in. 11 | more_locations: Option>, 12 | /// The search input, default will get all files from locations. 13 | search_input: Option, 14 | /// The file extension to search for, defaults to get all extensions. 15 | file_ext: Option, 16 | /// The depth to search to, defaults to no limit. 17 | depth: Option, 18 | /// The limit of results to return, defaults to no limit. 19 | limit: Option, 20 | /// When set to true, Searches for exact match, defaults to false. 21 | strict: bool, 22 | /// Set search option to be case insensitive, defaults to false. 23 | ignore_case: bool, 24 | /// Search for hidden files, defaults to false. 25 | hidden: bool, 26 | /// Filters Vector, defaults to empty vec 27 | filters: Vec, 28 | } 29 | 30 | impl SearchBuilder { 31 | /// Build a new [`Search`] instance. 32 | #[allow(deprecated)] 33 | pub fn build(&self) -> Search { 34 | Search::new( 35 | &self.search_location, 36 | self.more_locations.clone(), 37 | self.search_input.as_deref(), 38 | self.file_ext.as_deref(), 39 | self.depth, 40 | self.limit, 41 | self.strict, 42 | self.ignore_case, 43 | self.hidden, 44 | self.filters.clone(), 45 | ) 46 | } 47 | 48 | /// Set the search location to search in. 49 | /// ## Notes 50 | /// - Will replace `~` with [home directory](https://en.wikipedia.org/wiki/Home_directory) 51 | /// ### Arguments 52 | /// * `location` - The location to search in. 53 | /// ### Examples 54 | /// ```rust 55 | /// use rust_search::SearchBuilder; 56 | /// 57 | /// let search: Vec = SearchBuilder::default() 58 | /// .location("src") 59 | /// .build() 60 | /// .collect(); 61 | /// ``` 62 | pub fn location(mut self, location: impl AsRef) -> Self { 63 | self.search_location = replace_tilde_with_home_dir(location); 64 | self 65 | } 66 | 67 | /// Set the search input. 68 | /// ### Arguments 69 | /// * `input` - The search input. 70 | /// ### Examples 71 | /// ```rust 72 | /// use rust_search::SearchBuilder; 73 | /// 74 | /// let search: Vec = SearchBuilder::default() 75 | /// .search_input("Search") 76 | /// .build() 77 | /// .collect(); 78 | /// ``` 79 | pub fn search_input(mut self, input: impl Into) -> Self { 80 | self.search_input = Some(input.into()); 81 | self 82 | } 83 | 84 | /// Set the file extension to search for. 85 | /// ### Arguments 86 | /// * `ext` - The file extension to search for. 87 | /// ### Examples 88 | /// ```rust 89 | /// use rust_search::SearchBuilder; 90 | /// 91 | /// let search: Vec = SearchBuilder::default() 92 | /// .ext("rs") 93 | /// .build() 94 | /// .collect(); 95 | /// ``` 96 | pub fn ext(mut self, ext: impl Into) -> Self { 97 | let ext: String = ext.into(); 98 | // Remove the dot if it's there. 99 | self.file_ext = Some(ext.strip_prefix('.').map_or(ext.clone(), str::to_owned)); 100 | self 101 | } 102 | 103 | /// Add a filter to the search function. 104 | /// ### Arguments 105 | /// * `filter` - Closure getting dir: `DirEntry` variable to modify 106 | /// ### Examples 107 | /// ```rust 108 | /// use rust_search::{FileSize, FilterExt, SearchBuilder}; 109 | /// use std::time::{Duration, SystemTime}; 110 | /// 111 | /// let search: Vec = SearchBuilder::default() 112 | /// .location("~/path/to/directory") 113 | /// .file_size_greater(FileSize::Kilobyte(200.0)) 114 | /// .file_size_smaller(FileSize::Megabyte(10.0)) 115 | /// .created_after(SystemTime::now() - Duration::from_secs(3600 * 24 * 10)) 116 | /// .created_before(SystemTime::now()) 117 | /// .modified_after(SystemTime::now() - Duration::from_secs(3600 * 24 * 5)) 118 | /// .custom_filter(|dir| dir.metadata().unwrap().is_file()) 119 | /// .custom_filter(|dir| !dir.metadata().unwrap().permissions().readonly()) 120 | /// .build() 121 | /// .collect(); 122 | /// ``` 123 | pub fn filter(mut self, filter: FilterType) -> Self { 124 | self.filters.push(filter); 125 | self 126 | } 127 | 128 | /// Set the depth to search to, meaning how many subdirectories to search in. 129 | /// ### Arguments 130 | /// * `depth` - The depth to search to. 131 | /// ### Examples 132 | /// ```rust 133 | /// use rust_search::SearchBuilder; 134 | /// 135 | /// let search: Vec = SearchBuilder::default() 136 | /// .depth(1) 137 | /// .build() 138 | /// .collect(); 139 | /// ``` 140 | pub const fn depth(mut self, depth: usize) -> Self { 141 | self.depth = Some(depth); 142 | self 143 | } 144 | 145 | /// Set the limit of results to return. This will limit the amount of results returned. 146 | /// ### Arguments 147 | /// * `limit` - The limit of results to return. 148 | /// ### Examples 149 | /// ```rust 150 | /// use rust_search::SearchBuilder; 151 | /// 152 | /// let search: Vec = SearchBuilder::default() 153 | /// .limit(5) 154 | /// .build() 155 | /// .collect(); 156 | /// ``` 157 | pub const fn limit(mut self, limit: usize) -> Self { 158 | self.limit = Some(limit); 159 | self 160 | } 161 | 162 | /// Searches for exact match. 163 | /// 164 | /// For example, if the search input is "Search", the file "Search.rs" will be found, but not "Searcher.rs". 165 | /// ### Examples 166 | /// ```rust 167 | /// use rust_search::SearchBuilder; 168 | /// 169 | /// let search: Vec = SearchBuilder::default() 170 | /// .search_input("name") 171 | /// .strict() 172 | /// .build() 173 | /// .collect(); 174 | /// ``` 175 | pub const fn strict(mut self) -> Self { 176 | self.strict = true; 177 | self 178 | } 179 | 180 | /// Set search option to be case insensitive. 181 | /// 182 | /// For example, if the search input is "Search", the file "search.rs" will be found. 183 | /// ### Examples 184 | /// ```rust 185 | /// use rust_search::SearchBuilder; 186 | /// 187 | /// let search: Vec = SearchBuilder::default() 188 | /// .search_input("name") 189 | /// .ignore_case() 190 | /// .build() 191 | /// .collect(); 192 | /// ``` 193 | pub const fn ignore_case(mut self) -> Self { 194 | self.ignore_case = true; 195 | self 196 | } 197 | 198 | /// Searches for hidden files. 199 | /// ### Examples 200 | /// ```rust 201 | /// use rust_search::SearchBuilder; 202 | /// 203 | /// let search: Vec = SearchBuilder::default() 204 | /// .with_hidden() 205 | /// .build() 206 | /// .collect(); 207 | /// ``` 208 | pub const fn hidden(mut self) -> Self { 209 | self.hidden = true; 210 | self 211 | } 212 | 213 | /// Add extra locations to search in, in addition to the main location. 214 | /// ## Notes 215 | /// - Will replace `~` with [home directory](https://en.wikipedia.org/wiki/Home_directory) 216 | /// ### Arguments 217 | /// * `more_locations` - locations to search in. 218 | /// ### Examples 219 | /// ```rust 220 | /// use rust_search::SearchBuilder; 221 | /// 222 | /// let search: Vec = SearchBuilder::default() 223 | /// .more_locations(vec!["/Users/username/b/", "/Users/username/c/"]) 224 | /// .build() 225 | /// .collect(); 226 | /// ``` 227 | pub fn more_locations(mut self, more_locations: Vec>) -> Self { 228 | self.more_locations = Some( 229 | more_locations 230 | .into_iter() 231 | .map(replace_tilde_with_home_dir) 232 | .collect(), 233 | ); 234 | self 235 | } 236 | } 237 | 238 | impl Default for SearchBuilder { 239 | /// With this default, the search will get all files from the current directory. 240 | fn default() -> Self { 241 | Self { 242 | search_location: std::env::current_dir().expect("Failed to get current directory"), 243 | more_locations: None, 244 | search_input: None, 245 | file_ext: None, 246 | depth: None, 247 | limit: None, 248 | strict: false, 249 | ignore_case: false, 250 | hidden: false, 251 | filters: vec![], 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/filter.rs: -------------------------------------------------------------------------------- 1 | use super::SearchBuilder; 2 | use ignore::DirEntry; 3 | use std::{cmp::Ordering, time::SystemTime}; 4 | 5 | /// custom filter fn to expose the dir entry directly 6 | pub type FilterFn = fn(&DirEntry) -> bool; 7 | 8 | #[derive(Clone, Copy)] 9 | pub enum FilterType { 10 | Created(Ordering, SystemTime), 11 | Modified(Ordering, SystemTime), 12 | FileSize(Ordering, u64), 13 | Custom(FilterFn), 14 | } 15 | 16 | impl FilterType { 17 | pub fn apply(&self, dir: &DirEntry) -> bool { 18 | if let Ok(m) = dir.metadata() { 19 | match self { 20 | Self::Created(cmp, time) => { 21 | if let Ok(created) = m.created() { 22 | return created.cmp(time) == *cmp; 23 | } 24 | } 25 | Self::Modified(cmp, time) => { 26 | if let Ok(modified) = m.modified() { 27 | return modified.cmp(time) == *cmp; 28 | } 29 | } 30 | Self::FileSize(cmp, size_in_bytes) => { 31 | return m.len().cmp(size_in_bytes) == *cmp; 32 | } 33 | Self::Custom(f) => return f(dir), 34 | } 35 | } 36 | false 37 | } 38 | } 39 | 40 | /// enum to easily convert between `byte_sizes` 41 | #[derive(Debug, Clone)] 42 | pub enum FileSize { 43 | /// size in bytes 44 | Byte(u64), 45 | /// size in kilobytes 46 | Kilobyte(f64), 47 | /// size in megabytes 48 | Megabyte(f64), 49 | /// size in gigabytes 50 | Gigabyte(f64), 51 | /// size in terabytes 52 | Terabyte(f64), 53 | } 54 | 55 | // helper function for FileSize conversion 56 | fn convert(b: f64, pow: u32) -> u64 { 57 | (b * 1024_u64.pow(pow) as f64) as u64 58 | } 59 | 60 | #[allow(clippy::from_over_into)] 61 | impl Into for FileSize { 62 | fn into(self) -> u64 { 63 | use self::FileSize::{Byte, Gigabyte, Kilobyte, Megabyte, Terabyte}; 64 | 65 | match self { 66 | Byte(b) => b, 67 | Kilobyte(b) => convert(b, 1), 68 | Megabyte(b) => convert(b, 2), 69 | Gigabyte(b) => convert(b, 3), 70 | Terabyte(b) => convert(b, 4), 71 | } 72 | } 73 | } 74 | 75 | /// import this trait to filter files 76 | pub trait FilterExt { 77 | /// files created before `t`: [`SystemTime`] 78 | fn created_before(self, t: SystemTime) -> Self; 79 | /// files created at `t`: [`SystemTime`] 80 | fn created_at(self, t: SystemTime) -> Self; 81 | /// files created after `t`: [`SystemTime`] 82 | fn created_after(self, t: SystemTime) -> Self; 83 | /// files created before `t`: [`SystemTime`] 84 | fn modified_before(self, t: SystemTime) -> Self; 85 | /// files modified at `t`: [`SystemTime`] 86 | fn modified_at(self, t: SystemTime) -> Self; 87 | /// files modified after `t`: [`SystemTime`] 88 | fn modified_after(self, t: SystemTime) -> Self; 89 | /// files smaller than `size_in_bytes`: [usize] 90 | fn file_size_smaller(self, size: FileSize) -> Self; 91 | /// files equal to `size_in_bytes`: [usize] 92 | fn file_size_equal(self, size: FileSize) -> Self; 93 | /// files greater than `size_in_bytes`: [usize] 94 | fn file_size_greater(self, size: FileSize) -> Self; 95 | /// custom filter that exposes the [`DirEntry`] directly 96 | /// ```rust 97 | /// builder.custom_filter(|dir| dir.metadata().unwrap().is_file()) 98 | /// ``` 99 | fn custom_filter(self, f: FilterFn) -> Self; 100 | } 101 | 102 | use FilterType::{Created, Custom, FileSize as FilterFileSize, Modified}; 103 | use Ordering::{Equal, Greater, Less}; 104 | impl FilterExt for SearchBuilder { 105 | fn created_before(self, t: SystemTime) -> Self { 106 | self.filter(Created(Less, t)) 107 | } 108 | 109 | fn created_at(self, t: SystemTime) -> Self { 110 | self.filter(Created(Equal, t)) 111 | } 112 | 113 | fn created_after(self, t: SystemTime) -> Self { 114 | self.filter(Created(Greater, t)) 115 | } 116 | 117 | fn modified_before(self, t: SystemTime) -> Self { 118 | self.filter(Modified(Less, t)) 119 | } 120 | 121 | fn modified_at(self, t: SystemTime) -> Self { 122 | self.filter(Modified(Equal, t)) 123 | } 124 | 125 | fn modified_after(self, t: SystemTime) -> Self { 126 | self.filter(Modified(Greater, t)) 127 | } 128 | 129 | fn file_size_smaller(self, size: FileSize) -> Self { 130 | self.filter(FilterFileSize(Less, size.into())) 131 | } 132 | 133 | fn file_size_equal(self, size: FileSize) -> Self { 134 | self.filter(FilterFileSize(Equal, size.into())) 135 | } 136 | 137 | fn file_size_greater(self, size: FileSize) -> Self { 138 | self.filter(FilterFileSize(Greater, size.into())) 139 | } 140 | fn custom_filter(self, f: FilterFn) -> Self { 141 | self.filter(Custom(f)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::nursery, clippy::pedantic)] 2 | #![allow( 3 | clippy::cast_possible_truncation, 4 | clippy::cast_sign_loss, 5 | clippy::cast_precision_loss, 6 | clippy::module_name_repetitions, 7 | clippy::unused_self, 8 | clippy::return_self_not_must_use, 9 | clippy::must_use_candidate 10 | )] 11 | #![warn(missing_docs)] 12 | // Use the readme as the crate documentation 13 | #![doc = include_str!("../README.md")] 14 | 15 | mod builder; 16 | mod filter; 17 | mod search; 18 | mod utils; 19 | 20 | pub use builder::SearchBuilder; 21 | pub use filter::{FileSize, FilterExt, FilterFn}; 22 | 23 | // export this in order to use it with custom filter functions 24 | pub use ignore::DirEntry; 25 | pub use search::Search; 26 | pub use utils::similarity_sort; 27 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cmp, 3 | path::Path, 4 | sync::mpsc::{self, Sender}, 5 | }; 6 | 7 | use crate::{filter::FilterType, utils, SearchBuilder}; 8 | use ignore::{WalkBuilder, WalkState}; 9 | use regex::Regex; 10 | 11 | /// A struct that holds the receiver for the search results 12 | /// 13 | /// Can be iterated on to get the next element in the search results 14 | /// 15 | /// # Examples 16 | /// 17 | /// ## Iterate on the results 18 | /// 19 | /// ``` 20 | /// use rust_search::Search; 21 | /// 22 | /// let search = Search::new("src", None, Some(".rs"), Some(1)); 23 | /// 24 | /// for path in search { 25 | /// println!("{:?}", path); 26 | /// } 27 | /// ``` 28 | /// 29 | /// ## Collect results into a vector 30 | /// 31 | /// ``` 32 | /// use rust_search::Search; 33 | /// 34 | /// let search = Search::new("src", None, Some(".rs"), Some(1)); 35 | /// 36 | /// let paths_vec: Vec = search.collect(); 37 | /// ``` 38 | pub struct Search { 39 | rx: Box>, 40 | } 41 | 42 | impl Iterator for Search { 43 | type Item = String; 44 | 45 | fn next(&mut self) -> Option { 46 | self.rx.next() 47 | } 48 | } 49 | 50 | impl Search { 51 | /// Search for files in a given arguments 52 | /// ### Arguments 53 | /// * `search_location` - The location to search in 54 | /// * `search_input` - The search input, defaults to any word 55 | /// * `file_ext` - The file extension to search for, defaults to any file extension 56 | /// * `depth` - The depth to search to, defaults to no limit 57 | /// * `limit` - The limit of results to return, defaults to no limit 58 | /// * `strict` - Whether to search for the exact word or not 59 | /// * `ignore_case` - Whether to ignore case or not 60 | /// * `hidden` - Whether to search hidden files or not 61 | /// * `filters` - Vector of filters to search by `DirEntry` data 62 | #[allow(clippy::too_many_arguments)] 63 | pub(crate) fn new( 64 | search_location: impl AsRef, 65 | more_locations: Option>>, 66 | search_input: Option<&str>, 67 | file_ext: Option<&str>, 68 | depth: Option, 69 | limit: Option, 70 | strict: bool, 71 | ignore_case: bool, 72 | with_hidden: bool, 73 | filters: Vec, 74 | ) -> Self { 75 | let regex_search_input = 76 | utils::build_regex_search_input(search_input, file_ext, strict, ignore_case); 77 | 78 | let mut walker = WalkBuilder::new(search_location); 79 | 80 | walker 81 | .hidden(!with_hidden) 82 | .git_ignore(true) 83 | .max_depth(depth) 84 | .threads(cmp::min(12, num_cpus::get())); 85 | 86 | // filters getting applied to walker 87 | // only if all filters are true then the walker will return the file 88 | walker.filter_entry(move |dir| filters.iter().all(|f| f.apply(dir))); 89 | 90 | if let Some(locations) = more_locations { 91 | for location in locations { 92 | walker.add(location); 93 | } 94 | } 95 | 96 | let (tx, rx) = mpsc::channel::(); 97 | walker.build_parallel().run(|| { 98 | let tx: Sender = tx.clone(); 99 | let reg_exp: Regex = regex_search_input.clone(); 100 | let mut counter = 0; 101 | 102 | Box::new(move |path_entry| { 103 | if let Ok(entry) = path_entry { 104 | let path = entry.path(); 105 | if let Some(file_name) = path.file_name() { 106 | // Lossy means that if the file name is not valid UTF-8 107 | // it will be replaced with �. 108 | // Will return the file name with extension. 109 | let file_name = file_name.to_string_lossy().to_string(); 110 | if reg_exp.is_match(&file_name) { 111 | // Continue searching if the send was successful 112 | // and there is no limit or the limit has not been reached 113 | if tx.send(path.display().to_string()).is_ok() 114 | && (limit.is_none() || counter < limit.unwrap()) 115 | { 116 | counter += 1; 117 | return WalkState::Continue; 118 | } 119 | 120 | return WalkState::Quit; 121 | } 122 | } 123 | } 124 | WalkState::Continue 125 | }) 126 | }); 127 | 128 | if let Some(limit) = limit { 129 | // This will take the first `limit` elements from the iterator 130 | // will return all if there are less than `limit` elements 131 | Self { 132 | rx: Box::new(rx.into_iter().take(limit)), 133 | } 134 | } else { 135 | Self { 136 | rx: Box::new(rx.into_iter()), 137 | } 138 | } 139 | } 140 | } 141 | 142 | impl Default for Search { 143 | /// Effectively just creates a [`WalkBuilder`] over the current directory 144 | fn default() -> Self { 145 | SearchBuilder::default().build() 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::path::{Path, PathBuf}; 3 | use strsim::jaro_winkler; 4 | 5 | const FUZZY_SEARCH: &str = r".*"; 6 | 7 | pub fn build_regex_search_input( 8 | search_input: Option<&str>, 9 | file_ext: Option<&str>, 10 | strict: bool, 11 | ignore_case: bool, 12 | ) -> Regex { 13 | let file_type = file_ext.unwrap_or("*"); 14 | let search_input = search_input.unwrap_or(r"\w+"); 15 | 16 | let mut formatted_search_input = if strict { 17 | format!(r#"{search_input}\.{file_type}$"#) 18 | } else { 19 | format!(r#"{search_input}{FUZZY_SEARCH}\.{file_type}$"#) 20 | }; 21 | 22 | if ignore_case { 23 | formatted_search_input = set_case_insensitive(&formatted_search_input); 24 | } 25 | Regex::new(&formatted_search_input).unwrap() 26 | } 27 | 28 | fn set_case_insensitive(formatted_search_input: &str) -> String { 29 | "(?i)".to_owned() + formatted_search_input 30 | } 31 | 32 | /// Replace the tilde with the home directory, if it exists 33 | /// ### Arguments 34 | /// * `path` - The path to replace the tilde with the home directory 35 | pub fn replace_tilde_with_home_dir(path: impl AsRef) -> PathBuf { 36 | let path = path.as_ref(); 37 | if path.starts_with("~") { 38 | if let Some(home_dir) = dirs::home_dir() { 39 | // Remove the tilde from the path and append it to the home directory 40 | return home_dir.join(path.strip_prefix("~").unwrap()); 41 | } 42 | } 43 | path.to_path_buf() 44 | } 45 | 46 | fn file_name_from_path(path: &str) -> String { 47 | let path = Path::new(path); 48 | let file_name = path.file_name().unwrap().to_str().unwrap(); 49 | file_name.to_string() 50 | } 51 | 52 | /// This function can be used to sort the given vector on basis of similarity between the input & the vector 53 | /// 54 | /// ### Arguments 55 | /// * `&mut vector` - it needs a mutable reference to the vector 56 | /// ### Examples 57 | /// ```rust 58 | /// use rust_search::{SearchBuilder, similarity_sort}; 59 | /// 60 | /// let search_input = "fly"; 61 | /// let mut search: Vec = SearchBuilder::default() 62 | /// .location("~/Desktop/") 63 | /// .search_input(search_input) 64 | /// .depth(1) 65 | /// .ignore_case() 66 | /// .build() 67 | /// .collect(); 68 | 69 | /// similarity_sort(&mut search, &search_input); 70 | /// for path in search { 71 | /// println!("{:?}", path); 72 | /// } 73 | /// ``` 74 | /// 75 | /// search **without** similarity sort 76 | /// `["afly.txt", "bfly.txt", "flyer.txt", "fly.txt"]` 77 | /// 78 | /// search **with** similarity sort 79 | /// `["fly.txt", "flyer.txt", "afly.txt", "bfly.txt",]` 80 | /// 81 | /// ### Panics 82 | /// Will panic if `partial_cmp` is None 83 | pub fn similarity_sort(vector: &mut [String], input: &str) { 84 | vector.sort_by(|a, b| { 85 | let input = input.to_lowercase(); 86 | let a = file_name_from_path(a).to_lowercase(); 87 | let b = file_name_from_path(b).to_lowercase(); 88 | let a = jaro_winkler(a.as_str(), input.as_str()); 89 | let b = jaro_winkler(b.as_str(), input.as_str()); 90 | b.partial_cmp(&a).unwrap() 91 | }); 92 | } 93 | --------------------------------------------------------------------------------