Skip to content

Commit 65b0441

Browse files
committed
feat: add index command and implement logic to prune deleted files during directory indexing
1 parent 4281011 commit 65b0441

3 files changed

Lines changed: 65 additions & 13 deletions

File tree

src/main.rs

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std:: {
2-
fs::{self, File}, io::{self, BufReader, BufWriter, Read}, path::Path, process::{exit, ExitCode}, str::{self}, sync::{Arc, Mutex}, thread
2+
collections::HashSet, fs::{self, File}, io::{self, BufReader, BufWriter, Read}, path::{Path, PathBuf}, process::{exit, ExitCode}, str::{self}, sync::{Arc, Mutex}, thread
33
};
44

55
use parser::{Cli, Commands};
@@ -123,7 +123,7 @@ fn save_model_as_json(model: &InMemoryModel, index_path: &str) -> Result<(), ()>
123123
const ALLOWED_FILE_TYPE_EXTENSIONS: [&str; 5] = ["xml", "xhtml", "txt", "md", "pdf"];
124124

125125
/* Indexes a folder as a json file and adds to model, Processed is the number of file indexed */
126-
fn append_folder_to_model(dir_path: &Path, model: Arc<Mutex<InMemoryModel>>, processed: &mut usize) -> Result<(), ()> {
126+
fn append_folder_to_model(dir_path: &Path, model: Arc<Mutex<InMemoryModel>>, processed: &mut usize, visited: &mut HashSet<PathBuf>) -> Result<(), ()> {
127127
let dir = fs::read_dir(dir_path).map_err(|err| {
128128
eprintln!("{}: Failed to read directory {dir_path} as \"{err}\"", "ERROR".bold().red(),
129129
dir_path = dir_path.to_str().unwrap().bold().bright_blue(),
@@ -174,7 +174,7 @@ fn append_folder_to_model(dir_path: &Path, model: Arc<Mutex<InMemoryModel>>, pro
174174

175175
// Recursively index all the folders
176176
if file_type.is_dir() {
177-
append_folder_to_model(&file_path, Arc::clone(&model), processed)?;
177+
append_folder_to_model(&file_path, Arc::clone(&model), processed, visited)?;
178178
continue 'step;
179179
}
180180

@@ -184,6 +184,8 @@ fn append_folder_to_model(dir_path: &Path, model: Arc<Mutex<InMemoryModel>>, pro
184184
file_path.display().to_string().bright_blue(),
185185
err.to_string().red());
186186
})?;
187+
188+
visited.insert(file_path.clone());
187189

188190
// Main
189191
let mut model = model.lock().unwrap();
@@ -234,6 +236,42 @@ fn fetch_model(index_path: &str) -> Result<InMemoryModel, ()> {
234236
return Ok(model);
235237
}
236238

239+
fn index_directory(dir_path: &Path, model: Arc<Mutex<InMemoryModel>>, index_path: Option<&str>) -> Result<(), ()> {
240+
let root_dir = fs::canonicalize(dir_path).unwrap_or_else(|err| {
241+
eprintln!("{}: Could not canonicalize root dir {} as {}", "ERROR".bold().red(), dir_path.display().to_string().bright_blue(), err.to_string().red());
242+
exit(1);
243+
});
244+
245+
let mut processed: usize = 0;
246+
let mut visited: HashSet<PathBuf> = HashSet::new();
247+
248+
append_folder_to_model(&root_dir, Arc::clone(&model), &mut processed, &mut visited)?;
249+
250+
// Purge deleted files
251+
let mut model_lock = model.lock().unwrap();
252+
let mut to_remove = Vec::new();
253+
254+
for path in model_lock.docs.keys() {
255+
if path.starts_with(&root_dir) && !visited.contains(path) {
256+
to_remove.push(path.clone());
257+
}
258+
}
259+
260+
for path in to_remove {
261+
model_lock.remove_document(&path);
262+
println!("{}: Removed deleted file {}", "INFO".cyan(), path.display().to_string().bright_yellow());
263+
processed += 1;
264+
}
265+
266+
// Save the model only when some files were added/modified/deleted
267+
if processed > 0 {
268+
if let Some(p) = index_path {
269+
save_model_as_json(&model_lock, p)?;
270+
}
271+
}
272+
println!("{}: Finished indexing ...", "INFO".cyan());
273+
Ok(())
274+
}
237275

238276
#[derive(ValueEnum, Clone, Debug, PartialEq)]
239277
enum RankMethod {
@@ -259,6 +297,16 @@ fn entry() -> Result<(), ()> {
259297
check_index(&index_file_path).unwrap();
260298
}
261299

300+
Commands::Index { dir_path, output_file } => {
301+
let model = Arc::new(Mutex::new(Default::default()));
302+
let output_path = output_file.unwrap_or_else(|| {
303+
let mut p = Path::new(&dir_path).to_path_buf();
304+
p.push(".docsense.json");
305+
p.to_str().unwrap().to_string()
306+
});
307+
index_directory(Path::new(&dir_path), model, Some(&output_path))?;
308+
}
309+
262310
Commands::Serve { dir_path, address , rank_method} => {
263311
// IDEATE: Is it fine to place the index file in the folder itself or place in a root dir?
264312
let mut index_path = Path::new(&dir_path).to_path_buf();
@@ -285,15 +333,8 @@ fn entry() -> Result<(), ()> {
285333
let model = Arc::clone(&model);
286334
let dir_path = dir_path.clone();
287335
thread::spawn(move || {
288-
let mut processed: usize = 0 as usize;
289-
append_folder_to_model(Path::new(&dir_path), Arc::clone(&model), &mut processed).unwrap();
290-
291-
// Save the model only when some files are processed
292-
if processed > 0 {
293-
let model= model.lock().unwrap();
294-
save_model_as_json(&model, index_path.to_str().unwrap()).unwrap();
295-
}
296-
println!("{}: Finished indexing ...", "INFO".cyan());
336+
let index_str = index_path.to_str().unwrap().to_string();
337+
index_directory(Path::new(&dir_path), model, Some(&index_str)).unwrap();
297338
});
298339
}
299340
// TODO: Print the information of server start at the end of logging

src/model.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ fn idf(term: &str, model: &InMemoryModel) -> f32 {
9595
}
9696

9797
impl InMemoryModel {
98-
fn remove_document(&mut self, file_path: &Path) {
98+
pub fn remove_document(&mut self, file_path: &Path) {
9999
if let Some(doc) = self.docs.remove(file_path) {
100100
// Keep the cached total in sync
101101
self.total_tokens = self.total_tokens.saturating_sub(doc.count);

src/parser.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ pub enum Commands {
3333
index_file_path: String,
3434
},
3535

36+
#[command(
37+
about = "Index a directory for offline search",
38+
long_about = "Indexes the provided directory recursively and saves the model to a JSON file."
39+
)]
40+
Index {
41+
#[arg(help = "Path to directory to index")]
42+
dir_path: String,
43+
#[arg(help = "Path to save the generated index json file. Defaults to <dir_path>/.docsense.json")]
44+
output_file: Option<String>,
45+
},
46+
3647
#[command(
3748
about = "Serve directory over HTTP with search interface",
3849
long_about = "Indexes the provided directory recursively and starts a web server for querying indexed files through a UI."

0 commit comments

Comments
 (0)