| #![allow(dead_code)] |
|
|
| use std::sync::Arc; |
| use std::io::{self, Write}; |
| use parking_lot::RwLock; |
| use crossbeam_channel::unbounded; |
|
|
| use fastsearch::index::store::IndexStore; |
| use fastsearch::index::search::search; |
| use fastsearch::mft::reader::MftReader; |
| use fastsearch::mft::watcher::UsnWatcher; |
| use fastsearch::mft::types::IndexEvent; |
| use fastsearch::utils::drives::get_ntfs_drives; |
|
|
| fn main() { |
| println!("ββββββββββββββββββββββββββββββββββββ"); |
| println!("β FastSeek - File Search β"); |
| println!("ββββββββββββββββββββββββββββββββββββ"); |
| println!(); |
|
|
| let drives = get_ntfs_drives(); |
| if drives.is_empty() { |
| eprintln!("No NTFS drives found. Are you running as Administrator?"); |
| std::process::exit(1); |
| } |
|
|
| let index: Arc<RwLock<IndexStore>> = Arc::new(RwLock::new(IndexStore::new())); |
| let (tx, rx) = unbounded(); |
| let cache_path = std::env::temp_dir().join("fastseek_cache.bin"); |
|
|
| |
| let cache_loaded = if cache_path.exists() { |
| print!("Loading cached index... "); |
| io::stdout().flush().unwrap(); |
| match std::fs::read(&cache_path) { |
| Ok(compressed) => { |
| match lz4_flex::decompress_size_prepended(&compressed) { |
| Ok(bytes) => { |
| match bincode::deserialize::<fastsearch::index::store::CacheData>(&bytes) { |
| Ok(cache) => { |
| let count = cache.entries.len(); |
| let checkpoints = cache.checkpoints.clone(); |
| *index.write() = IndexStore::from_cache(cache); |
| println!("{} files", count); |
|
|
| |
| if !checkpoints.is_empty() { |
| print!("Catching up on changes since last run... "); |
| io::stdout().flush().unwrap(); |
|
|
| let (delta_tx, delta_rx) = unbounded::<IndexEvent>(); |
| let mut journal_ok = true; |
|
|
| for drive in &drives { |
| let cp = checkpoints.iter() |
| .find(|c| c.drive_letter == drive.letter); |
|
|
| if let Some(cp) = cp { |
| match UsnWatcher::new_from(drive, delta_tx.clone(), Some(cp)) { |
| Ok(mut watcher) => { |
| watcher.drain(); |
| let new_cp = watcher.checkpoint(); |
| let mut store = index.write(); |
| store.checkpoints.retain(|c| c.drive_letter != drive.letter); |
| store.checkpoints.push(new_cp); |
| } |
| Err(_) => { |
| println!("journal reset, full rescan needed."); |
| let _ = std::fs::remove_file(&cache_path); |
| journal_ok = false; |
| break; |
| } |
| } |
| } else { |
| |
| println!("missing checkpoint for {}:, full rescan needed.", drive.letter); |
| let _ = std::fs::remove_file(&cache_path); |
| journal_ok = false; |
| break; |
| } |
| } |
|
|
| drop(delta_tx); |
|
|
| if journal_ok { |
| let mut applied = 0usize; |
| let mut store = index.write(); |
| for event in delta_rx { |
| match event { |
| IndexEvent::Created(r) => store.insert(r), |
| IndexEvent::Deleted(id) => store.remove(id), |
| IndexEvent::Renamed { old_ref, new_record } => { |
| store.rename(old_ref, new_record) |
| } |
| IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => { |
| store.apply_move(file_ref, new_parent_ref, name, kind); |
| } |
| } |
| applied += 1; |
| } |
| println!("{} change(s) applied", applied); |
| println!(); |
| true |
| } else { |
| false |
| } |
| } else { |
| println!(); |
| true |
| } |
| } |
| Err(_) => { println!("cache corrupt, rescanning..."); false } |
| } |
| } |
| Err(_) => { println!("cache corrupt, rescanning..."); false } |
| } |
| } |
| Err(_) => { println!("cache unreadable, rescanning..."); false } |
| } |
| } else { |
| false |
| }; |
|
|
| |
| if !cache_loaded { |
| println!("Found drives: {}", drives.iter().map(|d| format!("{}:", d.letter)).collect::<Vec<_>>().join(", ")); |
| println!("Building index..."); |
|
|
| let total_start = std::time::Instant::now(); |
|
|
| |
| { |
| let mut store = index.write(); |
| for drive in &drives { |
| let (dummy_tx, _) = unbounded::<IndexEvent>(); |
| if let Ok(w) = UsnWatcher::new(drive, dummy_tx) { |
| store.checkpoints.push(w.checkpoint()); |
| } |
| } |
| } |
|
|
| let index_clone: Arc<RwLock<IndexStore>> = Arc::clone(&index); |
| let drives_clone = drives.clone(); |
|
|
| let scan_thread = std::thread::spawn(move || { |
| let mut total = 0usize; |
| let mut total_scan_time = std::time::Duration::ZERO; |
| let mut total_index_time = std::time::Duration::ZERO; |
|
|
| for drive in &drives_clone { |
| print!(" Scanning {}: ... ", drive.letter); |
| io::stdout().flush().unwrap(); |
|
|
| let reader: MftReader = match MftReader::open(drive) { |
| Ok(r) => r, |
| Err(e) => { println!("FAILED ({:?})", e); continue; } |
| }; |
|
|
| let t1 = std::time::Instant::now(); |
| let (scan, method) = match reader.scan_direct() { |
| Some(s) => (s, "direct"), |
| None => (reader.scan(), "ioctl"), |
| }; |
| let count = scan.records.len(); |
| let scan_time = t1.elapsed(); |
|
|
| let t2 = std::time::Instant::now(); |
| { |
| let mut store = index_clone.write(); |
| store.populate_from_scan(scan, &drive.root); |
| } |
| let index_time = t2.elapsed(); |
|
|
| println!("{} files (scan {:.2}s {}, index {:.2}s)", |
| count, scan_time.as_secs_f64(), method, index_time.as_secs_f64()); |
|
|
| total += count; |
| total_scan_time += scan_time; |
| total_index_time += index_time; |
| } |
|
|
| { |
| let mut store = index_clone.write(); |
| store.finalize(); |
| } |
|
|
| println!(); |
| println!("Index ready β {} total files (scan {:.2}s, index {:.2}s)", |
| total, total_scan_time.as_secs_f64(), total_index_time.as_secs_f64()); |
| total |
| }); |
|
|
| scan_thread.join().unwrap(); |
|
|
| |
| { |
| let store = index.read(); |
| let cache = store.to_cache(); |
| match bincode::serialize(&cache) { |
| Ok(bytes) => { |
| let raw_mb = bytes.len() as f64 / 1_048_576.0; |
| let compressed = lz4_flex::compress_prepend_size(&bytes); |
| let comp_mb = compressed.len() as f64 / 1_048_576.0; |
| match std::fs::write(&cache_path, &compressed) { |
| Ok(_) => println!("Cache saved β {:.1}MB compressed ({:.1}MB raw)", comp_mb, raw_mb), |
| Err(e) => eprintln!("Could not save cache: {}", e), |
| } |
| } |
| Err(e) => eprintln!("Could not serialize: {}", e), |
| } |
| } |
|
|
| let total_elapsed = total_start.elapsed(); |
| println!("Total startup: {:.2}s", total_elapsed.as_secs_f64()); |
| println!(); |
| } |
|
|
| |
| let live_checkpoints: Arc<parking_lot::Mutex<Vec<fastsearch::mft::types::JournalCheckpoint>>> = |
| Arc::new(parking_lot::Mutex::new(index.read().checkpoints.clone())); |
|
|
| for drive in &drives { |
| let tx_clone = tx.clone(); |
| let drive_clone = drive.clone(); |
| let cps = Arc::clone(&live_checkpoints); |
| std::thread::spawn(move || { |
| if let Ok(mut watcher) = UsnWatcher::new(&drive_clone, tx_clone) { |
| watcher.run_shared(cps); |
| } |
| }); |
| } |
|
|
| |
| let index_live: Arc<RwLock<IndexStore>> = Arc::clone(&index); |
| std::thread::spawn(move || { |
| for event in rx { |
| let mut store = index_live.write(); |
| match event { |
| IndexEvent::Created(r) => store.insert(r), |
| IndexEvent::Deleted(id) => store.remove(id), |
| IndexEvent::Renamed { old_ref, new_record } => store.rename(old_ref, new_record), |
| IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => { |
| store.apply_move(file_ref, new_parent_ref, name, kind); |
| } |
| } |
| } |
| }); |
|
|
| |
| let index_for_save = Arc::clone(&index); |
| let cps_for_save = Arc::clone(&live_checkpoints); |
| ctrlc::set_handler(move || { |
| let mut store = index_for_save.write(); |
| store.checkpoints = cps_for_save.lock().clone(); |
| let cache = store.to_cache(); |
| if let Ok(bytes) = bincode::serialize(&cache) { |
| let compressed = lz4_flex::compress_prepend_size(&bytes); |
| let _ = std::fs::write( |
| std::env::temp_dir().join("fastseek_cache.bin"), |
| &compressed, |
| ); |
| } |
| std::process::exit(0); |
| }).ok(); |
|
|
| search_loop(index); |
| } |
|
|
| fn search_loop(index: Arc<RwLock<IndexStore>>) { |
| let config_path = config_dir().join("config.txt"); |
| let mut case_sensitive = false; |
| let mut excluded_dirs: Vec<String> = load_exclusions(&config_path); |
|
|
| println!("Commands:"); |
| println!(" <query> search files"); |
| println!(" folder:<query> directories only (or :<query>)"); |
| println!(" file:<query> files only (or !<query>)"); |
| println!(" *.ext / ext:ext by extension e.g. *.pdf, ext:docx"); |
| println!(" case toggle case sensitivity [off]"); |
| println!(" exclude <path> exclude a directory"); |
| println!(" unexclude <path> remove exclusion"); |
| println!(" exclusions list excluded dirs"); |
| println!(" count total indexed files"); |
| println!(" rescan clear cache and rescan"); |
| println!(" quit exit"); |
| println!(); |
|
|
| loop { |
| print!("search> "); |
| io::stdout().flush().unwrap(); |
|
|
| let mut input = String::new(); |
| match io::stdin().read_line(&mut input) { |
| Ok(0) | Err(_) => break, |
| Ok(_) => {} |
| } |
|
|
| let input = input.trim(); |
| if input.is_empty() { continue; } |
|
|
| match input { |
| "quit" | "exit" | "q" => { |
| println!("Bye."); |
| break; |
| } |
|
|
| "count" => { |
| let store = index.read(); |
| println!(" {} files in index\n", store.len()); |
| } |
|
|
| "rescan" => { |
| let cache_path = std::env::temp_dir().join("fastseek_cache.bin"); |
| let _ = std::fs::remove_file(&cache_path); |
| println!("Cache cleared. Restart fastseek to rescan.\n"); |
| } |
|
|
| "case" => { |
| case_sensitive = !case_sensitive; |
| println!(" case sensitivity: {}\n", if case_sensitive { "ON" } else { "OFF" }); |
| } |
|
|
| "exclusions" => { |
| if excluded_dirs.is_empty() { |
| println!(" no excluded directories\n"); |
| } else { |
| println!(); |
| for d in &excluded_dirs { |
| println!(" - {}", d); |
| } |
| println!(); |
| } |
| } |
|
|
| _ if input.starts_with("exclude ") => { |
| let path = input[8..].trim().to_lowercase(); |
| if !path.is_empty() { |
| let path = if path.ends_with('\\') || path.ends_with('/') { |
| path |
| } else { |
| format!("{}\\", path) |
| }; |
| if !excluded_dirs.contains(&path) { |
| excluded_dirs.push(path.clone()); |
| save_exclusions(&config_path, &excluded_dirs); |
| } |
| println!(" excluded: {}\n", path); |
| } |
| } |
|
|
| _ if input.starts_with("unexclude ") => { |
| let path = input[10..].trim().to_lowercase(); |
| let path = if path.ends_with('\\') || path.ends_with('/') { |
| path |
| } else { |
| format!("{}\\", path) |
| }; |
| let before = excluded_dirs.len(); |
| excluded_dirs.retain(|d| d != &path); |
| save_exclusions(&config_path, &excluded_dirs); |
| if excluded_dirs.len() < before { |
| println!(" removed: {}\n", path); |
| } else { |
| println!(" not found in exclusions\n"); |
| } |
| } |
|
|
| _ => { |
| let parsed = parse_query(input); |
|
|
| let store = index.read(); |
| let start = std::time::Instant::now(); |
|
|
| let results: Vec<_> = if let Some(ref ext) = parsed.ext_filter { |
| use fastsearch::index::search::SearchResult; |
| let dot_ext = format!(".{}", ext); |
| store.entries.iter().filter_map(|entry| { |
| let name = store.name_lower(entry); |
| if !name.ends_with(&dot_ext) { |
| return None; |
| } |
| let kind_ok = match parsed.filter { |
| Filter::All => true, |
| Filter::Dirs => matches!(entry.kind(), fastsearch::mft::types::FileKind::Directory), |
| Filter::Files => !matches!(entry.kind(), fastsearch::mft::types::FileKind::Directory), |
| }; |
| if !kind_ok { return None; } |
|
|
| let full_path = fastsearch::index::search::build_path( |
| entry.file_ref, &store |
| ); |
|
|
| |
| if !excluded_dirs.is_empty() { |
| let path_lower = full_path.to_string_lossy().to_lowercase(); |
| for ex in &excluded_dirs { |
| if path_lower.starts_with(ex.as_str()) { |
| return None; |
| } |
| } |
| } |
|
|
| Some(SearchResult { |
| full_path, |
| name: store.name(entry).to_string(), |
| rank: 0, |
| is_dir: matches!(entry.kind(), fastsearch::mft::types::FileKind::Directory), |
| }) |
| }).take(50).collect() |
| } else { |
| let raw = search( |
| &store, |
| parsed.query, |
| 200, |
| case_sensitive, |
| &excluded_dirs, |
| ); |
| raw.into_iter().filter(|r| { |
| match parsed.filter { |
| Filter::All => true, |
| Filter::Dirs => r.is_dir, |
| Filter::Files => !r.is_dir, |
| } |
| }).take(50).collect() |
| }; |
| let elapsed = start.elapsed(); |
|
|
| if results.is_empty() { |
| println!(" no results for \"{}\"\n", input); |
| } else { |
| println!(); |
| for (i, r) in results.iter().enumerate() { |
| let kind = if r.is_dir { "DIR " } else { "FILE" }; |
| println!(" [{:>3}] [{}] {}", i + 1, kind, r.full_path.display()); |
| } |
| println!(); |
| println!(" {} result(s) in {:.2}ms\n", |
| results.len(), elapsed.as_secs_f64() * 1000.0); |
| } |
| } |
| } |
| } |
| } |
|
|
| enum Filter { All, Dirs, Files } |
|
|
| struct ParsedQuery<'a> { |
| query: &'a str, |
| filter: Filter, |
| ext_filter: Option<String>, |
| } |
|
|
| fn parse_query(input: &str) -> ParsedQuery<'_> { |
| |
| if let Some(ext) = input.strip_prefix("ext:") { |
| return ParsedQuery { query: "", filter: Filter::Files, ext_filter: Some(ext.to_lowercase()) }; |
| } |
| if input.starts_with("*.") { |
| return ParsedQuery { query: "", filter: Filter::All, ext_filter: Some(input[2..].to_lowercase()) }; |
| } |
| |
| if let Some(q) = input.strip_prefix("folder:") { |
| return ParsedQuery { query: q.trim(), filter: Filter::Dirs, ext_filter: None }; |
| } |
| if let Some(q) = input.strip_prefix("file:") { |
| return ParsedQuery { query: q.trim(), filter: Filter::Files, ext_filter: None }; |
| } |
| |
| if let Some(q) = input.strip_prefix(':') { |
| return ParsedQuery { query: q, filter: Filter::Dirs, ext_filter: None }; |
| } |
| if let Some(q) = input.strip_prefix('!') { |
| return ParsedQuery { query: q, filter: Filter::Files, ext_filter: None }; |
| } |
| ParsedQuery { query: input, filter: Filter::All, ext_filter: None } |
| } |
|
|
| fn config_dir() -> std::path::PathBuf { |
| let dir = std::env::var("APPDATA") |
| .map(std::path::PathBuf::from) |
| .unwrap_or_else(|_| std::env::temp_dir()) |
| .join("fastsearch"); |
| let _ = std::fs::create_dir_all(&dir); |
| dir |
| } |
|
|
| fn load_exclusions(path: &std::path::Path) -> Vec<String> { |
| std::fs::read_to_string(path) |
| .unwrap_or_default() |
| .lines() |
| .map(|l| l.trim().to_lowercase()) |
| .filter(|l| !l.is_empty()) |
| .collect() |
| } |
|
|
| fn save_exclusions(path: &std::path::Path, dirs: &[String]) { |
| let content: String = dirs.join("\n"); |
| let _ = std::fs::write(path, content); |
| } |
|
|