anshdadhich commited on
Commit
797025a
·
verified ·
1 Parent(s): 2f60eb8

Upload src/main.rs

Browse files
Files changed (1) hide show
  1. src/main.rs +499 -0
src/main.rs ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #![allow(dead_code)]
2
+
3
+ use std::sync::Arc;
4
+ use std::io::{self, Write};
5
+ use parking_lot::RwLock;
6
+ use crossbeam_channel::unbounded;
7
+
8
+ use fastsearch::index::store::IndexStore;
9
+ use fastsearch::index::search::search;
10
+ use fastsearch::mft::reader::MftReader;
11
+ use fastsearch::mft::watcher::UsnWatcher;
12
+ use fastsearch::mft::types::IndexEvent;
13
+ use fastsearch::utils::drives::get_ntfs_drives;
14
+
15
+ fn main() {
16
+ println!("╔══════════════════════════════════╗");
17
+ println!("║ FastSeek - File Search ║");
18
+ println!("╚══════════════════════════════════╝");
19
+ println!();
20
+
21
+ let drives = get_ntfs_drives();
22
+ if drives.is_empty() {
23
+ eprintln!("No NTFS drives found. Are you running as Administrator?");
24
+ std::process::exit(1);
25
+ }
26
+
27
+ let index: Arc<RwLock<IndexStore>> = Arc::new(RwLock::new(IndexStore::new()));
28
+ let (tx, rx) = unbounded();
29
+ let cache_path = std::env::temp_dir().join("fastseek_cache.bin");
30
+
31
+ // --- Try loading from cache ---
32
+ let cache_loaded = if cache_path.exists() {
33
+ print!("Loading cached index... ");
34
+ io::stdout().flush().unwrap();
35
+ match std::fs::read(&cache_path) {
36
+ Ok(compressed) => {
37
+ match lz4_flex::decompress_size_prepended(&compressed) {
38
+ Ok(bytes) => {
39
+ match bincode::deserialize::<fastsearch::index::store::CacheData>(&bytes) {
40
+ Ok(cache) => {
41
+ let count = cache.entries.len();
42
+ let checkpoints = cache.checkpoints.clone();
43
+ *index.write() = IndexStore::from_cache(cache);
44
+ println!("{} files", count);
45
+
46
+ // --- Delta catch-up ---
47
+ if !checkpoints.is_empty() {
48
+ print!("Catching up on changes since last run... ");
49
+ io::stdout().flush().unwrap();
50
+
51
+ let (delta_tx, delta_rx) = unbounded::<IndexEvent>();
52
+ let mut journal_ok = true;
53
+
54
+ for drive in &drives {
55
+ let cp = checkpoints.iter()
56
+ .find(|c| c.drive_letter == drive.letter);
57
+
58
+ if let Some(cp) = cp {
59
+ match UsnWatcher::new_from(drive, delta_tx.clone(), Some(cp)) {
60
+ Ok(mut watcher) => {
61
+ watcher.drain();
62
+ let new_cp = watcher.checkpoint();
63
+ let mut store = index.write();
64
+ store.checkpoints.retain(|c| c.drive_letter != drive.letter);
65
+ store.checkpoints.push(new_cp);
66
+ }
67
+ Err(_) => {
68
+ println!("journal reset, full rescan needed.");
69
+ let _ = std::fs::remove_file(&cache_path);
70
+ journal_ok = false;
71
+ break;
72
+ }
73
+ }
74
+ } else {
75
+ // No checkpoint for this drive — cache is incomplete
76
+ println!("missing checkpoint for {}:, full rescan needed.", drive.letter);
77
+ let _ = std::fs::remove_file(&cache_path);
78
+ journal_ok = false;
79
+ break;
80
+ }
81
+ }
82
+
83
+ drop(delta_tx);
84
+
85
+ if journal_ok {
86
+ let mut applied = 0usize;
87
+ let mut store = index.write();
88
+ for event in delta_rx {
89
+ match event {
90
+ IndexEvent::Created(r) => store.insert(r),
91
+ IndexEvent::Deleted(id) => store.remove(id),
92
+ IndexEvent::Renamed { old_ref, new_record } => {
93
+ store.rename(old_ref, new_record)
94
+ }
95
+ IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => {
96
+ store.apply_move(file_ref, new_parent_ref, name, kind);
97
+ }
98
+ }
99
+ applied += 1;
100
+ }
101
+ println!("{} change(s) applied", applied);
102
+ println!();
103
+ true
104
+ } else {
105
+ false
106
+ }
107
+ } else {
108
+ println!();
109
+ true
110
+ }
111
+ }
112
+ Err(_) => { println!("cache corrupt, rescanning..."); false }
113
+ }
114
+ }
115
+ Err(_) => { println!("cache corrupt, rescanning..."); false }
116
+ }
117
+ }
118
+ Err(_) => { println!("cache unreadable, rescanning..."); false }
119
+ }
120
+ } else {
121
+ false
122
+ };
123
+
124
+ // --- Full MFT scan if no cache ---
125
+ if !cache_loaded {
126
+ println!("Found drives: {}", drives.iter().map(|d| format!("{}:", d.letter)).collect::<Vec<_>>().join(", "));
127
+ println!("Building index...");
128
+
129
+ let total_start = std::time::Instant::now();
130
+
131
+ // Capture checkpoints BEFORE scan so changes during scan aren't lost
132
+ {
133
+ let mut store = index.write();
134
+ for drive in &drives {
135
+ let (dummy_tx, _) = unbounded::<IndexEvent>();
136
+ if let Ok(w) = UsnWatcher::new(drive, dummy_tx) {
137
+ store.checkpoints.push(w.checkpoint());
138
+ }
139
+ }
140
+ }
141
+
142
+ let index_clone: Arc<RwLock<IndexStore>> = Arc::clone(&index);
143
+ let drives_clone = drives.clone();
144
+
145
+ let scan_thread = std::thread::spawn(move || {
146
+ let mut total = 0usize;
147
+ let mut total_scan_time = std::time::Duration::ZERO;
148
+ let mut total_index_time = std::time::Duration::ZERO;
149
+
150
+ for drive in &drives_clone {
151
+ print!(" Scanning {}: ... ", drive.letter);
152
+ io::stdout().flush().unwrap();
153
+
154
+ let reader: MftReader = match MftReader::open(drive) {
155
+ Ok(r) => r,
156
+ Err(e) => { println!("FAILED ({:?})", e); continue; }
157
+ };
158
+
159
+ let t1 = std::time::Instant::now();
160
+ let (scan, method) = match reader.scan_direct() {
161
+ Some(s) => (s, "direct"),
162
+ None => (reader.scan(), "ioctl"),
163
+ };
164
+ let count = scan.records.len();
165
+ let scan_time = t1.elapsed();
166
+
167
+ let t2 = std::time::Instant::now();
168
+ {
169
+ let mut store = index_clone.write();
170
+ store.populate_from_scan(scan, &drive.root);
171
+ }
172
+ let index_time = t2.elapsed();
173
+
174
+ println!("{} files (scan {:.2}s {}, index {:.2}s)",
175
+ count, scan_time.as_secs_f64(), method, index_time.as_secs_f64());
176
+
177
+ total += count;
178
+ total_scan_time += scan_time;
179
+ total_index_time += index_time;
180
+ }
181
+
182
+ {
183
+ let mut store = index_clone.write();
184
+ store.finalize();
185
+ }
186
+
187
+ println!();
188
+ println!("Index ready — {} total files (scan {:.2}s, index {:.2}s)",
189
+ total, total_scan_time.as_secs_f64(), total_index_time.as_secs_f64());
190
+ total
191
+ });
192
+
193
+ scan_thread.join().unwrap();
194
+
195
+ // Save cache
196
+ {
197
+ let store = index.read();
198
+ let cache = store.to_cache();
199
+ match bincode::serialize(&cache) {
200
+ Ok(bytes) => {
201
+ let raw_mb = bytes.len() as f64 / 1_048_576.0;
202
+ let compressed = lz4_flex::compress_prepend_size(&bytes);
203
+ let comp_mb = compressed.len() as f64 / 1_048_576.0;
204
+ match std::fs::write(&cache_path, &compressed) {
205
+ Ok(_) => println!("Cache saved — {:.1}MB compressed ({:.1}MB raw)", comp_mb, raw_mb),
206
+ Err(e) => eprintln!("Could not save cache: {}", e),
207
+ }
208
+ }
209
+ Err(e) => eprintln!("Could not serialize: {}", e),
210
+ }
211
+ }
212
+
213
+ let total_elapsed = total_start.elapsed();
214
+ println!("Total startup: {:.2}s", total_elapsed.as_secs_f64());
215
+ println!();
216
+ }
217
+
218
+ // --- USN watchers for live updates while running ---
219
+ let live_checkpoints: Arc<parking_lot::Mutex<Vec<fastsearch::mft::types::JournalCheckpoint>>> =
220
+ Arc::new(parking_lot::Mutex::new(index.read().checkpoints.clone()));
221
+
222
+ for drive in &drives {
223
+ let tx_clone = tx.clone();
224
+ let drive_clone = drive.clone();
225
+ let cps = Arc::clone(&live_checkpoints);
226
+ std::thread::spawn(move || {
227
+ if let Ok(mut watcher) = UsnWatcher::new(&drive_clone, tx_clone) {
228
+ watcher.run_shared(cps);
229
+ }
230
+ });
231
+ }
232
+
233
+ // --- Live index updates ---
234
+ let index_live: Arc<RwLock<IndexStore>> = Arc::clone(&index);
235
+ std::thread::spawn(move || {
236
+ for event in rx {
237
+ let mut store = index_live.write();
238
+ match event {
239
+ IndexEvent::Created(r) => store.insert(r),
240
+ IndexEvent::Deleted(id) => store.remove(id),
241
+ IndexEvent::Renamed { old_ref, new_record } => store.rename(old_ref, new_record),
242
+ IndexEvent::Moved { file_ref, new_parent_ref, name, kind } => {
243
+ store.apply_move(file_ref, new_parent_ref, name, kind);
244
+ }
245
+ }
246
+ }
247
+ });
248
+
249
+ // Save updated cache on exit with latest checkpoints from live watchers
250
+ let index_for_save = Arc::clone(&index);
251
+ let cps_for_save = Arc::clone(&live_checkpoints);
252
+ ctrlc::set_handler(move || {
253
+ let mut store = index_for_save.write();
254
+ store.checkpoints = cps_for_save.lock().clone();
255
+ let cache = store.to_cache();
256
+ if let Ok(bytes) = bincode::serialize(&cache) {
257
+ let compressed = lz4_flex::compress_prepend_size(&bytes);
258
+ let _ = std::fs::write(
259
+ std::env::temp_dir().join("fastseek_cache.bin"),
260
+ &compressed,
261
+ );
262
+ }
263
+ std::process::exit(0);
264
+ }).ok();
265
+
266
+ search_loop(index);
267
+ }
268
+
269
+ fn search_loop(index: Arc<RwLock<IndexStore>>) {
270
+ let config_path = config_dir().join("config.txt");
271
+ let mut case_sensitive = false;
272
+ let mut excluded_dirs: Vec<String> = load_exclusions(&config_path);
273
+
274
+ println!("Commands:");
275
+ println!(" <query> search files");
276
+ println!(" folder:<query> directories only (or :<query>)");
277
+ println!(" file:<query> files only (or !<query>)");
278
+ println!(" *.ext / ext:ext by extension e.g. *.pdf, ext:docx");
279
+ println!(" case toggle case sensitivity [off]");
280
+ println!(" exclude <path> exclude a directory");
281
+ println!(" unexclude <path> remove exclusion");
282
+ println!(" exclusions list excluded dirs");
283
+ println!(" count total indexed files");
284
+ println!(" rescan clear cache and rescan");
285
+ println!(" quit exit");
286
+ println!();
287
+
288
+ loop {
289
+ print!("search> ");
290
+ io::stdout().flush().unwrap();
291
+
292
+ let mut input = String::new();
293
+ match io::stdin().read_line(&mut input) {
294
+ Ok(0) | Err(_) => break,
295
+ Ok(_) => {}
296
+ }
297
+
298
+ let input = input.trim();
299
+ if input.is_empty() { continue; }
300
+
301
+ match input {
302
+ "quit" | "exit" | "q" => {
303
+ println!("Bye.");
304
+ break;
305
+ }
306
+
307
+ "count" => {
308
+ let store = index.read();
309
+ println!(" {} files in index\n", store.len());
310
+ }
311
+
312
+ "rescan" => {
313
+ let cache_path = std::env::temp_dir().join("fastseek_cache.bin");
314
+ let _ = std::fs::remove_file(&cache_path);
315
+ println!("Cache cleared. Restart fastseek to rescan.\n");
316
+ }
317
+
318
+ "case" => {
319
+ case_sensitive = !case_sensitive;
320
+ println!(" case sensitivity: {}\n", if case_sensitive { "ON" } else { "OFF" });
321
+ }
322
+
323
+ "exclusions" => {
324
+ if excluded_dirs.is_empty() {
325
+ println!(" no excluded directories\n");
326
+ } else {
327
+ println!();
328
+ for d in &excluded_dirs {
329
+ println!(" - {}", d);
330
+ }
331
+ println!();
332
+ }
333
+ }
334
+
335
+ _ if input.starts_with("exclude ") => {
336
+ let path = input[8..].trim().to_lowercase();
337
+ if !path.is_empty() {
338
+ let path = if path.ends_with('\\') || path.ends_with('/') {
339
+ path
340
+ } else {
341
+ format!("{}\\", path)
342
+ };
343
+ if !excluded_dirs.contains(&path) {
344
+ excluded_dirs.push(path.clone());
345
+ save_exclusions(&config_path, &excluded_dirs);
346
+ }
347
+ println!(" excluded: {}\n", path);
348
+ }
349
+ }
350
+
351
+ _ if input.starts_with("unexclude ") => {
352
+ let path = input[10..].trim().to_lowercase();
353
+ let path = if path.ends_with('\\') || path.ends_with('/') {
354
+ path
355
+ } else {
356
+ format!("{}\\", path)
357
+ };
358
+ let before = excluded_dirs.len();
359
+ excluded_dirs.retain(|d| d != &path);
360
+ save_exclusions(&config_path, &excluded_dirs);
361
+ if excluded_dirs.len() < before {
362
+ println!(" removed: {}\n", path);
363
+ } else {
364
+ println!(" not found in exclusions\n");
365
+ }
366
+ }
367
+
368
+ _ => {
369
+ let parsed = parse_query(input);
370
+
371
+ let store = index.read();
372
+ let start = std::time::Instant::now();
373
+
374
+ let results: Vec<_> = if let Some(ref ext) = parsed.ext_filter {
375
+ use fastsearch::index::search::SearchResult;
376
+ let dot_ext = format!(".{}", ext);
377
+ store.entries.iter().filter_map(|entry| {
378
+ let name = store.name_lower(entry);
379
+ if !name.ends_with(&dot_ext) {
380
+ return None;
381
+ }
382
+ let kind_ok = match parsed.filter {
383
+ Filter::All => true,
384
+ Filter::Dirs => matches!(entry.kind(), fastsearch::mft::types::FileKind::Directory),
385
+ Filter::Files => !matches!(entry.kind(), fastsearch::mft::types::FileKind::Directory),
386
+ };
387
+ if !kind_ok { return None; }
388
+
389
+ let full_path = fastsearch::index::search::build_path(
390
+ entry.file_ref, &store
391
+ );
392
+
393
+ // Check exclusions
394
+ if !excluded_dirs.is_empty() {
395
+ let path_lower = full_path.to_string_lossy().to_lowercase();
396
+ for ex in &excluded_dirs {
397
+ if path_lower.starts_with(ex.as_str()) {
398
+ return None;
399
+ }
400
+ }
401
+ }
402
+
403
+ Some(SearchResult {
404
+ full_path,
405
+ name: store.name(entry).to_string(),
406
+ rank: 0,
407
+ is_dir: matches!(entry.kind(), fastsearch::mft::types::FileKind::Directory),
408
+ })
409
+ }).take(50).collect()
410
+ } else {
411
+ let raw = search(
412
+ &store,
413
+ parsed.query,
414
+ 200,
415
+ case_sensitive,
416
+ &excluded_dirs,
417
+ );
418
+ raw.into_iter().filter(|r| {
419
+ match parsed.filter {
420
+ Filter::All => true,
421
+ Filter::Dirs => r.is_dir,
422
+ Filter::Files => !r.is_dir,
423
+ }
424
+ }).take(50).collect()
425
+ };
426
+ let elapsed = start.elapsed();
427
+
428
+ if results.is_empty() {
429
+ println!(" no results for \"{}\"\n", input);
430
+ } else {
431
+ println!();
432
+ for (i, r) in results.iter().enumerate() {
433
+ let kind = if r.is_dir { "DIR " } else { "FILE" };
434
+ println!(" [{:>3}] [{}] {}", i + 1, kind, r.full_path.display());
435
+ }
436
+ println!();
437
+ println!(" {} result(s) in {:.2}ms\n",
438
+ results.len(), elapsed.as_secs_f64() * 1000.0);
439
+ }
440
+ }
441
+ }
442
+ }
443
+ }
444
+
445
+ enum Filter { All, Dirs, Files }
446
+
447
+ struct ParsedQuery<'a> {
448
+ query: &'a str,
449
+ filter: Filter,
450
+ ext_filter: Option<String>,
451
+ }
452
+
453
+ fn parse_query(input: &str) -> ParsedQuery<'_> {
454
+ // ext:pdf or *.pdf
455
+ if let Some(ext) = input.strip_prefix("ext:") {
456
+ return ParsedQuery { query: "", filter: Filter::Files, ext_filter: Some(ext.to_lowercase()) };
457
+ }
458
+ if input.starts_with("*.") {
459
+ return ParsedQuery { query: "", filter: Filter::All, ext_filter: Some(input[2..].to_lowercase()) };
460
+ }
461
+ // folder:name / file:name
462
+ if let Some(q) = input.strip_prefix("folder:") {
463
+ return ParsedQuery { query: q.trim(), filter: Filter::Dirs, ext_filter: None };
464
+ }
465
+ if let Some(q) = input.strip_prefix("file:") {
466
+ return ParsedQuery { query: q.trim(), filter: Filter::Files, ext_filter: None };
467
+ }
468
+ // existing shortcuts
469
+ if let Some(q) = input.strip_prefix(':') {
470
+ return ParsedQuery { query: q, filter: Filter::Dirs, ext_filter: None };
471
+ }
472
+ if let Some(q) = input.strip_prefix('!') {
473
+ return ParsedQuery { query: q, filter: Filter::Files, ext_filter: None };
474
+ }
475
+ ParsedQuery { query: input, filter: Filter::All, ext_filter: None }
476
+ }
477
+
478
+ fn config_dir() -> std::path::PathBuf {
479
+ let dir = std::env::var("APPDATA")
480
+ .map(std::path::PathBuf::from)
481
+ .unwrap_or_else(|_| std::env::temp_dir())
482
+ .join("fastsearch");
483
+ let _ = std::fs::create_dir_all(&dir);
484
+ dir
485
+ }
486
+
487
+ fn load_exclusions(path: &std::path::Path) -> Vec<String> {
488
+ std::fs::read_to_string(path)
489
+ .unwrap_or_default()
490
+ .lines()
491
+ .map(|l| l.trim().to_lowercase())
492
+ .filter(|l| !l.is_empty())
493
+ .collect()
494
+ }
495
+
496
+ fn save_exclusions(path: &std::path::Path, dirs: &[String]) {
497
+ let content: String = dirs.join("\n");
498
+ let _ = std::fs::write(path, content);
499
+ }