From 50594e3923d9a3870dd767725b9c375c7101d003 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:16:59 +0000 Subject: [PATCH 1/7] Initial plan From 517da322fa05df7f2c072bf85ce29c3cc2a6757f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:28:57 +0000 Subject: [PATCH 2/7] Add generate_database_from_files function and tests Co-authored-by: ploiu <43047560+ploiu@users.noreply.github.com> --- src/repository/mod.rs | 134 +++++ .../tests/generate_database_from_files.rs | 508 ++++++++++++++++++ src/repository/tests/mod.rs | 1 + 3 files changed, 643 insertions(+) create mode 100644 src/repository/tests/generate_database_from_files.rs create mode 100644 src/repository/tests/mod.rs diff --git a/src/repository/mod.rs b/src/repository/mod.rs index c9e52e7..7e13c1b 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -1,3 +1,4 @@ +use std::fs; use std::path::Path; #[cfg(not(test))] @@ -5,11 +6,17 @@ use rusqlite::OpenFlags; use rusqlite::{Connection, Result}; use crate::db_migrations::migrate_db; +use crate::model::file_types::FileTypes; +use crate::model::repository::{FileRecord, Folder}; +use crate::service::file_service::{determine_file_type, file_dir}; pub mod file_repository; pub mod folder_repository; pub mod metadata_repository; +#[cfg(test)] +mod tests; + /// creates a new connection and returns it, but panics if the connection could not be created #[cfg(not(test))] pub fn open_connection() -> Connection { @@ -43,16 +50,143 @@ fn create_db(con: &mut Connection) { /// If not, it either creates or upgrades the database accordingly pub fn initialize_db() -> Result<()> { let mut con = open_connection(); + let mut should_gen_database_from_files = false; // table_version will be used once we have more versions of the database let table_version = match metadata_repository::get_version(&con) { Ok(value) => value.parse::().unwrap(), Err(_) => { // tables haven't been created yet create_db(&mut con); + should_gen_database_from_files = true; 1 } }; migrate_db(&con, table_version)?; + if should_gen_database_from_files { + generate_database_from_files(None, &con)?; + } con.close().unwrap(); Ok(()) } + +/// Generates database entries from the existing files directory structure. +/// This walks the directory tree depth-first, creating folders before files at each level. +/// +/// # Arguments +/// * `parent_folder` - The parent folder id in the database, or None for root level +/// * `con` - Database connection +/// +/// # Returns +/// * `Result<()>` - Ok if successful, or a rusqlite error +pub fn generate_database_from_files( + parent_folder: Option, + con: &Connection, +) -> Result<()> { + let base_path = if parent_folder.is_none() { + file_dir() + } else { + // For recursive calls, we need to build the path from the parent folder + // This is handled by passing the path directly via internal helper + return Ok(()); + }; + + let path = Path::new(&base_path); + if !path.exists() || !path.is_dir() { + return Ok(()); + } + + // Check if directory is empty + let entries: Vec<_> = match fs::read_dir(path) { + Ok(iter) => iter.filter_map(|e| e.ok()).collect(), + Err(_) => return Ok(()), + }; + + if entries.is_empty() { + return Ok(()); + } + + generate_database_from_files_internal(&base_path, parent_folder, con) +} + +/// Internal helper that walks the directory tree and creates database entries. +/// Walks depth-first, creating folders first at each level before files. +fn generate_database_from_files_internal( + current_path: &str, + parent_folder: Option, + con: &Connection, +) -> Result<()> { + let path = Path::new(current_path); + + let entries: Vec<_> = match fs::read_dir(path) { + Ok(iter) => iter.filter_map(|e| e.ok()).collect(), + Err(_) => return Ok(()), + }; + + // Separate folders and files + let mut folders: Vec<_> = entries.iter().filter(|e| { + e.file_type().map(|ft| ft.is_dir()).unwrap_or(false) + }).collect(); + let mut files: Vec<_> = entries.iter().filter(|e| { + e.file_type().map(|ft| ft.is_file()).unwrap_or(false) + }).collect(); + + // Sort for consistent ordering + folders.sort_by_key(|e| e.file_name()); + files.sort_by_key(|e| e.file_name()); + + // Process folders first (depth-first: process each folder fully before moving to next) + for folder_entry in folders { + let folder_name = folder_entry.file_name().to_string_lossy().to_string(); + + // Create folder in database + let folder = Folder { + id: None, + name: folder_name, + parent_id: parent_folder, + }; + + let created_folder = folder_repository::create_folder(&folder, con)?; + let folder_id = created_folder.id; + + // Recursively process this folder's contents (depth-first) + let child_path = folder_entry.path(); + generate_database_from_files_internal( + child_path.to_str().unwrap_or(""), + folder_id, + con, + )?; + } + + // Then process files at this level + for file_entry in files { + let file_name = file_entry.file_name().to_string_lossy().to_string(); + let file_path = file_entry.path(); + + // Get file size + let file_size = fs::metadata(&file_path) + .map(|m| m.len()) + .unwrap_or(0); + + // Determine file type + let file_type: FileTypes = determine_file_type(&file_name); + + // Create file record + let file_record = FileRecord { + id: None, + name: file_name, + parent_id: parent_folder, + create_date: chrono::offset::Local::now().naive_local(), + size: file_size, + file_type, + }; + + let file_id = file_repository::create_file(&file_record, con)?; + + // Link file to folder if not at root level + if let Some(folder_id) = parent_folder { + folder_repository::link_folder_to_file(file_id, folder_id, con)?; + } + } + + Ok(()) +} diff --git a/src/repository/tests/generate_database_from_files.rs b/src/repository/tests/generate_database_from_files.rs new file mode 100644 index 0000000..c69db3b --- /dev/null +++ b/src/repository/tests/generate_database_from_files.rs @@ -0,0 +1,508 @@ +use crate::repository::{file_repository, folder_repository, initialize_db, open_connection}; +use crate::service::file_service::file_dir; +use crate::test::{cleanup, create_file_disk, create_folder_disk}; + +mod generate_database_from_files_basic { + use super::*; + + #[test] + fn empty_files_directory_returns_ok() { + cleanup(); + // Create empty files directory + std::fs::create_dir_all(file_dir()).unwrap(); + initialize_db().unwrap(); + + let con = open_connection(); + // Should not have created any folders or files + let folders = folder_repository::get_child_folders(None, &con).unwrap(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + assert!(folders.is_empty()); + assert!(files.is_empty()); + + cleanup(); + } + + #[test] + fn missing_files_directory_returns_ok() { + cleanup(); + // Don't create files directory at all + initialize_db().unwrap(); + + let con = open_connection(); + // Should not have created any folders or files + let folders = folder_repository::get_child_folders(None, &con).unwrap(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + assert!(folders.is_empty()); + assert!(files.is_empty()); + + cleanup(); + } +} + +mod generate_database_from_files_single_level { + use super::*; + + #[test] + fn creates_single_file_at_root() { + cleanup(); + create_file_disk("test.txt", "test content"); + initialize_db().unwrap(); + + let con = open_connection(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(files.len(), 1); + assert_eq!(files[0].name, "test.txt"); + + cleanup(); + } + + #[test] + fn creates_single_folder_at_root() { + cleanup(); + create_folder_disk("folder1"); + initialize_db().unwrap(); + + let con = open_connection(); + let folders = folder_repository::get_child_folders(None, &con).unwrap(); + con.close().unwrap(); + + assert_eq!(folders.len(), 1); + assert_eq!(folders[0].name, "folder1"); + + cleanup(); + } + + #[test] + fn creates_multiple_files_at_root() { + cleanup(); + create_file_disk("test1.txt", "content1"); + create_file_disk("test2.png", "content2"); + create_file_disk("test3.mp4", "content3"); + initialize_db().unwrap(); + + let con = open_connection(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(files.len(), 3); + let file_names: Vec<&str> = files.iter().map(|f| f.name.as_str()).collect(); + assert!(file_names.contains(&"test1.txt")); + assert!(file_names.contains(&"test2.png")); + assert!(file_names.contains(&"test3.mp4")); + + cleanup(); + } + + #[test] + fn creates_multiple_folders_at_root() { + cleanup(); + create_folder_disk("folder1"); + create_folder_disk("folder2"); + create_folder_disk("folder3"); + initialize_db().unwrap(); + + let con = open_connection(); + let folders = folder_repository::get_child_folders(None, &con).unwrap(); + con.close().unwrap(); + + assert_eq!(folders.len(), 3); + let folder_names: Vec<&str> = folders.iter().map(|f| f.name.as_str()).collect(); + assert!(folder_names.contains(&"folder1")); + assert!(folder_names.contains(&"folder2")); + assert!(folder_names.contains(&"folder3")); + + cleanup(); + } + + #[test] + fn creates_files_and_folders_at_root() { + cleanup(); + create_folder_disk("folder1"); + create_folder_disk("folder2"); + create_file_disk("file1.txt", "content1"); + create_file_disk("file2.png", "content2"); + initialize_db().unwrap(); + + let con = open_connection(); + let folders = folder_repository::get_child_folders(None, &con).unwrap(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(folders.len(), 2); + assert_eq!(files.len(), 2); + + cleanup(); + } +} + +mod generate_database_from_files_nested { + use super::*; + + #[test] + fn creates_nested_folder_with_file() { + cleanup(); + create_folder_disk("parent"); + create_file_disk("parent/child.txt", "child content"); + initialize_db().unwrap(); + + let con = open_connection(); + let root_folders = folder_repository::get_child_folders(None, &con).unwrap(); + assert_eq!(root_folders.len(), 1); + assert_eq!(root_folders[0].name, "parent"); + + let parent_id = root_folders[0].id.unwrap(); + let child_files = folder_repository::get_child_files(&[parent_id], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(child_files.len(), 1); + assert_eq!(child_files[0].name, "child.txt"); + assert_eq!(child_files[0].parent_id, Some(parent_id)); + + cleanup(); + } + + #[test] + fn creates_nested_folders() { + cleanup(); + create_folder_disk("parent"); + create_folder_disk("parent/child"); + create_file_disk("parent/child/grandchild.txt", "content"); + initialize_db().unwrap(); + + let con = open_connection(); + let root_folders = folder_repository::get_child_folders(None, &con).unwrap(); + assert_eq!(root_folders.len(), 1); + + let parent_id = root_folders[0].id.unwrap(); + let child_folders = folder_repository::get_child_folders(Some(parent_id), &con).unwrap(); + assert_eq!(child_folders.len(), 1); + // Note: folder name is the full path in the database + assert_eq!(child_folders[0].name, "parent/child"); + + let child_id = child_folders[0].id.unwrap(); + let grandchild_files = folder_repository::get_child_files(&[child_id], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(grandchild_files.len(), 1); + assert_eq!(grandchild_files[0].name, "grandchild.txt"); + + cleanup(); + } +} + +mod generate_database_from_files_deep_nesting { + use super::*; + + #[test] + fn handles_6_levels_deep() { + cleanup(); + // Create a 6-level deep structure + create_folder_disk("level1"); + create_folder_disk("level1/level2"); + create_folder_disk("level1/level2/level3"); + create_folder_disk("level1/level2/level3/level4"); + create_folder_disk("level1/level2/level3/level4/level5"); + create_folder_disk("level1/level2/level3/level4/level5/level6"); + create_file_disk("level1/level2/level3/level4/level5/level6/deep_file.txt", "deep content"); + initialize_db().unwrap(); + + let con = open_connection(); + + // Verify level 1 + let level1_folders = folder_repository::get_child_folders(None, &con).unwrap(); + assert_eq!(level1_folders.len(), 1); + assert_eq!(level1_folders[0].name, "level1"); + let level1_id = level1_folders[0].id.unwrap(); + + // Verify level 2 (folder name is full path from root) + let level2_folders = folder_repository::get_child_folders(Some(level1_id), &con).unwrap(); + assert_eq!(level2_folders.len(), 1); + assert_eq!(level2_folders[0].name, "level1/level2"); + let level2_id = level2_folders[0].id.unwrap(); + + // Verify level 3 + let level3_folders = folder_repository::get_child_folders(Some(level2_id), &con).unwrap(); + assert_eq!(level3_folders.len(), 1); + assert_eq!(level3_folders[0].name, "level1/level2/level3"); + let level3_id = level3_folders[0].id.unwrap(); + + // Verify level 4 + let level4_folders = folder_repository::get_child_folders(Some(level3_id), &con).unwrap(); + assert_eq!(level4_folders.len(), 1); + assert_eq!(level4_folders[0].name, "level1/level2/level3/level4"); + let level4_id = level4_folders[0].id.unwrap(); + + // Verify level 5 + let level5_folders = folder_repository::get_child_folders(Some(level4_id), &con).unwrap(); + assert_eq!(level5_folders.len(), 1); + assert_eq!(level5_folders[0].name, "level1/level2/level3/level4/level5"); + let level5_id = level5_folders[0].id.unwrap(); + + // Verify level 6 + let level6_folders = folder_repository::get_child_folders(Some(level5_id), &con).unwrap(); + assert_eq!(level6_folders.len(), 1); + assert_eq!(level6_folders[0].name, "level1/level2/level3/level4/level5/level6"); + let level6_id = level6_folders[0].id.unwrap(); + + // Verify deepest file + let deep_files = folder_repository::get_child_files(&[level6_id], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(deep_files.len(), 1); + assert_eq!(deep_files[0].name, "deep_file.txt"); + assert_eq!(deep_files[0].parent_id, Some(level6_id)); + + cleanup(); + } +} + +mod generate_database_from_files_file_properties { + use crate::model::file_types::FileTypes; + + use super::*; + + #[test] + fn correctly_determines_file_type() { + cleanup(); + create_file_disk("test.txt", "text"); + create_file_disk("test.png", "image"); + create_file_disk("test.mp4", "video"); + initialize_db().unwrap(); + + let con = open_connection(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + let txt_file = files.iter().find(|f| f.name == "test.txt").unwrap(); + let png_file = files.iter().find(|f| f.name == "test.png").unwrap(); + let mp4_file = files.iter().find(|f| f.name == "test.mp4").unwrap(); + + assert_eq!(txt_file.file_type, FileTypes::Text); + assert_eq!(png_file.file_type, FileTypes::Image); + assert_eq!(mp4_file.file_type, FileTypes::Video); + + cleanup(); + } + + #[test] + fn correctly_stores_file_size() { + cleanup(); + let content = "test content with specific size"; + create_file_disk("sized.txt", content); + initialize_db().unwrap(); + + let con = open_connection(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(files.len(), 1); + assert_eq!(files[0].size, content.len() as u64); + + cleanup(); + } +} + +mod generate_database_from_files_complex_structures { + use super::*; + + #[test] + fn creates_breadth_structure() { + cleanup(); + // Create multiple folders with multiple files each + create_folder_disk("folder_a"); + create_folder_disk("folder_b"); + create_folder_disk("folder_c"); + create_file_disk("folder_a/file_a1.txt", "content"); + create_file_disk("folder_a/file_a2.txt", "content"); + create_file_disk("folder_b/file_b1.txt", "content"); + create_file_disk("folder_c/file_c1.txt", "content"); + create_file_disk("folder_c/file_c2.txt", "content"); + create_file_disk("folder_c/file_c3.txt", "content"); + initialize_db().unwrap(); + + let con = open_connection(); + let root_folders = folder_repository::get_child_folders(None, &con).unwrap(); + assert_eq!(root_folders.len(), 3); + + // Find each folder by name and verify its contents + let folder_a = root_folders.iter().find(|f| f.name == "folder_a").unwrap(); + let folder_b = root_folders.iter().find(|f| f.name == "folder_b").unwrap(); + let folder_c = root_folders.iter().find(|f| f.name == "folder_c").unwrap(); + + let files_a = folder_repository::get_child_files(&[folder_a.id.unwrap()], &con).unwrap(); + let files_b = folder_repository::get_child_files(&[folder_b.id.unwrap()], &con).unwrap(); + let files_c = folder_repository::get_child_files(&[folder_c.id.unwrap()], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(files_a.len(), 2); + assert_eq!(files_b.len(), 1); + assert_eq!(files_c.len(), 3); + + cleanup(); + } +} + +mod generate_database_existing_db { + use crate::test::init_db_folder; + use super::*; + + #[test] + fn does_not_regenerate_when_db_exists() { + cleanup(); + // First, create database with init_db_folder (which uses initialize_db) + init_db_folder(); + + // Manually create a file in the files directory AFTER db is initialized + create_file_disk("new_file.txt", "new content"); + + // Call initialize_db again - it should NOT regenerate from files + initialize_db().unwrap(); + + let con = open_connection(); + // Verify the new_file.txt is NOT in the database + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + // The file should not be in the database because we didn't regenerate + assert!( + !files.iter().any(|f| f.name == "new_file.txt"), + "File should not be in database because db already existed" + ); + + cleanup(); + } + + #[test] + fn preserves_existing_data() { + cleanup(); + // First create db and add some data + init_db_folder(); + + // Create a file entry in the db (not on disk) + let con = open_connection(); + file_repository::create_file( + &crate::model::repository::FileRecord { + id: None, + name: "existing_file.txt".to_string(), + parent_id: None, + create_date: chrono::offset::Local::now().naive_local(), + size: 100, + file_type: crate::model::file_types::FileTypes::Text, + }, + &con, + ) + .unwrap(); + con.close().unwrap(); + + // Create a file on disk that we want to make sure doesn't get added + create_file_disk("disk_only.txt", "disk content"); + + // Call initialize_db again + initialize_db().unwrap(); + + let con = open_connection(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + // The existing file should still be there + assert!( + files.iter().any(|f| f.name == "existing_file.txt"), + "Existing file should still be in database" + ); + // The disk-only file should NOT be added + assert!( + !files.iter().any(|f| f.name == "disk_only.txt"), + "Disk-only file should not have been added to existing database" + ); + + cleanup(); + } + + #[test] + fn regeneration_only_happens_on_fresh_db() { + cleanup(); + // Create files on disk + create_file_disk("fresh_file.txt", "fresh content"); + + // Initialize db (fresh) - should create file entry + initialize_db().unwrap(); + + let con = open_connection(); + let files = folder_repository::get_child_files(&[], &con).unwrap(); + con.close().unwrap(); + + // The file should be in the database because it was a fresh db + assert!( + files.iter().any(|f| f.name == "fresh_file.txt"), + "File should be in database for fresh db" + ); + + cleanup(); + } +} + +mod generate_database_verifies_all_files { + use super::*; + + #[test] + fn all_files_at_various_levels_are_in_database() { + cleanup(); + // Create a mixed structure + create_file_disk("root1.txt", "root1"); + create_file_disk("root2.png", "root2"); + create_folder_disk("folder1"); + create_file_disk("folder1/level1_file1.txt", "l1f1"); + create_file_disk("folder1/level1_file2.txt", "l1f2"); + create_folder_disk("folder1/subfolder"); + create_file_disk("folder1/subfolder/level2_file.txt", "l2f"); + create_folder_disk("folder2"); + create_file_disk("folder2/another.txt", "another"); + + initialize_db().unwrap(); + + let con = open_connection(); + + // Check root files + let root_files = folder_repository::get_child_files(&[], &con).unwrap(); + assert_eq!(root_files.len(), 2); + assert!(root_files.iter().any(|f| f.name == "root1.txt")); + assert!(root_files.iter().any(|f| f.name == "root2.png")); + + // Check root folders + let root_folders = folder_repository::get_child_folders(None, &con).unwrap(); + assert_eq!(root_folders.len(), 2); + + // Check folder1 contents + let folder1 = root_folders.iter().find(|f| f.name == "folder1").unwrap(); + let folder1_id = folder1.id.unwrap(); + let folder1_files = folder_repository::get_child_files(&[folder1_id], &con).unwrap(); + assert_eq!(folder1_files.len(), 2); + assert!(folder1_files.iter().any(|f| f.name == "level1_file1.txt")); + assert!(folder1_files.iter().any(|f| f.name == "level1_file2.txt")); + + // Check subfolder contents + let subfolder = folder_repository::get_child_folders(Some(folder1_id), &con).unwrap(); + assert_eq!(subfolder.len(), 1); + let subfolder_id = subfolder[0].id.unwrap(); + let subfolder_files = folder_repository::get_child_files(&[subfolder_id], &con).unwrap(); + assert_eq!(subfolder_files.len(), 1); + assert_eq!(subfolder_files[0].name, "level2_file.txt"); + + // Check folder2 contents + let folder2 = root_folders.iter().find(|f| f.name == "folder2").unwrap(); + let folder2_id = folder2.id.unwrap(); + let folder2_files = folder_repository::get_child_files(&[folder2_id], &con).unwrap(); + con.close().unwrap(); + + assert_eq!(folder2_files.len(), 1); + assert_eq!(folder2_files[0].name, "another.txt"); + + cleanup(); + } +} diff --git a/src/repository/tests/mod.rs b/src/repository/tests/mod.rs new file mode 100644 index 0000000..44b3db7 --- /dev/null +++ b/src/repository/tests/mod.rs @@ -0,0 +1 @@ +mod generate_database_from_files; From c578646f9ebea19c1b1eb3f9a2f70a3805ab2f32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:35:20 +0000 Subject: [PATCH 3/7] Improve documentation and clarity of generate_database_from_files function Co-authored-by: ploiu <43047560+ploiu@users.noreply.github.com> --- src/repository/mod.rs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 7e13c1b..4373401 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -72,8 +72,12 @@ pub fn initialize_db() -> Result<()> { /// Generates database entries from the existing files directory structure. /// This walks the directory tree depth-first, creating folders before files at each level. /// +/// This function is designed to be called with `parent_folder = None` to start the +/// generation from the root files directory. The `parent_folder` parameter exists +/// to satisfy the API contract but the actual recursive traversal is handled internally. +/// /// # Arguments -/// * `parent_folder` - The parent folder id in the database, or None for root level +/// * `parent_folder` - Should be None to start from root. Any other value is a no-op. /// * `con` - Database connection /// /// # Returns @@ -82,14 +86,12 @@ pub fn generate_database_from_files( parent_folder: Option, con: &Connection, ) -> Result<()> { - let base_path = if parent_folder.is_none() { - file_dir() - } else { - // For recursive calls, we need to build the path from the parent folder - // This is handled by passing the path directly via internal helper + // This function only processes the root level; recursion is handled internally + if parent_folder.is_some() { return Ok(()); - }; + } + let base_path = file_dir(); let path = Path::new(&base_path); if !path.exists() || !path.is_dir() { return Ok(()); @@ -105,7 +107,7 @@ pub fn generate_database_from_files( return Ok(()); } - generate_database_from_files_internal(&base_path, parent_folder, con) + generate_database_from_files_internal(&base_path, None, con) } /// Internal helper that walks the directory tree and creates database entries. From 5ad8582503a9332f89f4ded4422258ef1db5d320 Mon Sep 17 00:00:00 2001 From: ploiu Date: Tue, 25 Nov 2025 20:52:49 +0000 Subject: [PATCH 4/7] clean up some tests --- src/repository/mod.rs | 65 +++++++++---------- .../tests/generate_database_from_files.rs | 40 +++--------- 2 files changed, 38 insertions(+), 67 deletions(-) diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 4373401..4c618c6 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -82,31 +82,28 @@ pub fn initialize_db() -> Result<()> { /// /// # Returns /// * `Result<()>` - Ok if successful, or a rusqlite error -pub fn generate_database_from_files( - parent_folder: Option, - con: &Connection, -) -> Result<()> { +pub fn generate_database_from_files(parent_folder: Option, con: &Connection) -> Result<()> { // This function only processes the root level; recursion is handled internally if parent_folder.is_some() { return Ok(()); } - + let base_path = file_dir(); let path = Path::new(&base_path); if !path.exists() || !path.is_dir() { return Ok(()); } - + // Check if directory is empty let entries: Vec<_> = match fs::read_dir(path) { Ok(iter) => iter.filter_map(|e| e.ok()).collect(), Err(_) => return Ok(()), }; - + if entries.is_empty() { return Ok(()); } - + generate_database_from_files_internal(&base_path, None, con) } @@ -118,60 +115,56 @@ fn generate_database_from_files_internal( con: &Connection, ) -> Result<()> { let path = Path::new(current_path); - + let entries: Vec<_> = match fs::read_dir(path) { Ok(iter) => iter.filter_map(|e| e.ok()).collect(), Err(_) => return Ok(()), }; - + // Separate folders and files - let mut folders: Vec<_> = entries.iter().filter(|e| { - e.file_type().map(|ft| ft.is_dir()).unwrap_or(false) - }).collect(); - let mut files: Vec<_> = entries.iter().filter(|e| { - e.file_type().map(|ft| ft.is_file()).unwrap_or(false) - }).collect(); - + let mut folders: Vec<_> = entries + .iter() + .filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) + .collect(); + let mut files: Vec<_> = entries + .iter() + .filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false)) + .collect(); + // Sort for consistent ordering folders.sort_by_key(|e| e.file_name()); files.sort_by_key(|e| e.file_name()); - + // Process folders first (depth-first: process each folder fully before moving to next) for folder_entry in folders { let folder_name = folder_entry.file_name().to_string_lossy().to_string(); - + // Create folder in database let folder = Folder { id: None, name: folder_name, parent_id: parent_folder, }; - + let created_folder = folder_repository::create_folder(&folder, con)?; let folder_id = created_folder.id; - + // Recursively process this folder's contents (depth-first) let child_path = folder_entry.path(); - generate_database_from_files_internal( - child_path.to_str().unwrap_or(""), - folder_id, - con, - )?; + generate_database_from_files_internal(child_path.to_str().unwrap_or(""), folder_id, con)?; } - + // Then process files at this level for file_entry in files { let file_name = file_entry.file_name().to_string_lossy().to_string(); let file_path = file_entry.path(); - + // Get file size - let file_size = fs::metadata(&file_path) - .map(|m| m.len()) - .unwrap_or(0); - + let file_size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or(0); + // Determine file type let file_type: FileTypes = determine_file_type(&file_name); - + // Create file record let file_record = FileRecord { id: None, @@ -181,14 +174,14 @@ fn generate_database_from_files_internal( size: file_size, file_type, }; - + let file_id = file_repository::create_file(&file_record, con)?; - + // Link file to folder if not at root level if let Some(folder_id) = parent_folder { folder_repository::link_folder_to_file(file_id, folder_id, con)?; } } - + Ok(()) } diff --git a/src/repository/tests/generate_database_from_files.rs b/src/repository/tests/generate_database_from_files.rs index c69db3b..8445724 100644 --- a/src/repository/tests/generate_database_from_files.rs +++ b/src/repository/tests/generate_database_from_files.rs @@ -170,7 +170,6 @@ mod generate_database_from_files_nested { #[test] fn creates_nested_folders() { cleanup(); - create_folder_disk("parent"); create_folder_disk("parent/child"); create_file_disk("parent/child/grandchild.txt", "content"); initialize_db().unwrap(); @@ -203,13 +202,11 @@ mod generate_database_from_files_deep_nesting { fn handles_6_levels_deep() { cleanup(); // Create a 6-level deep structure - create_folder_disk("level1"); - create_folder_disk("level1/level2"); - create_folder_disk("level1/level2/level3"); - create_folder_disk("level1/level2/level3/level4"); - create_folder_disk("level1/level2/level3/level4/level5"); create_folder_disk("level1/level2/level3/level4/level5/level6"); - create_file_disk("level1/level2/level3/level4/level5/level6/deep_file.txt", "deep content"); + create_file_disk( + "level1/level2/level3/level4/level5/level6/deep_file.txt", + "deep content", + ); initialize_db().unwrap(); let con = open_connection(); @@ -247,7 +244,10 @@ mod generate_database_from_files_deep_nesting { // Verify level 6 let level6_folders = folder_repository::get_child_folders(Some(level5_id), &con).unwrap(); assert_eq!(level6_folders.len(), 1); - assert_eq!(level6_folders[0].name, "level1/level2/level3/level4/level5/level6"); + assert_eq!( + level6_folders[0].name, + "level1/level2/level3/level4/level5/level6" + ); let level6_id = level6_folders[0].id.unwrap(); // Verify deepest file @@ -349,8 +349,8 @@ mod generate_database_from_files_complex_structures { } mod generate_database_existing_db { - use crate::test::init_db_folder; use super::*; + use crate::test::init_db_folder; #[test] fn does_not_regenerate_when_db_exists() { @@ -423,28 +423,6 @@ mod generate_database_existing_db { cleanup(); } - - #[test] - fn regeneration_only_happens_on_fresh_db() { - cleanup(); - // Create files on disk - create_file_disk("fresh_file.txt", "fresh content"); - - // Initialize db (fresh) - should create file entry - initialize_db().unwrap(); - - let con = open_connection(); - let files = folder_repository::get_child_files(&[], &con).unwrap(); - con.close().unwrap(); - - // The file should be in the database because it was a fresh db - assert!( - files.iter().any(|f| f.name == "fresh_file.txt"), - "File should be in database for fresh db" - ); - - cleanup(); - } } mod generate_database_verifies_all_files { From 60bef5bbf71f111d8060177013d83211f23096bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:05:49 +0000 Subject: [PATCH 5/7] Address PR review comments: add logging, remove sorting, queue icons Co-authored-by: ploiu <43047560+ploiu@users.noreply.github.com> --- src/repository/mod.rs | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 4c618c6..5c40fa2 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -8,6 +8,7 @@ use rusqlite::{Connection, Result}; use crate::db_migrations::migrate_db; use crate::model::file_types::FileTypes; use crate::model::repository::{FileRecord, Folder}; +use crate::queue; use crate::service::file_service::{determine_file_type, file_dir}; pub mod file_repository; @@ -104,7 +105,12 @@ pub fn generate_database_from_files(parent_folder: Option, con: &Connection return Ok(()); } - generate_database_from_files_internal(&base_path, None, con) + // Magenta text for starting job + log::info!("\x1b[35mGenerating database from file system\x1b[0m"); + let result = generate_database_from_files_internal(&base_path, None, con); + // Green text for finished job + log::info!("\x1b[32mFinished generating database from file system\x1b[0m"); + result } /// Internal helper that walks the directory tree and creates database entries. @@ -122,27 +128,26 @@ fn generate_database_from_files_internal( }; // Separate folders and files - let mut folders: Vec<_> = entries + let folders: Vec<_> = entries .iter() .filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false)) .collect(); - let mut files: Vec<_> = entries + let files: Vec<_> = entries .iter() .filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false)) .collect(); - // Sort for consistent ordering - folders.sort_by_key(|e| e.file_name()); - files.sort_by_key(|e| e.file_name()); - // Process folders first (depth-first: process each folder fully before moving to next) for folder_entry in folders { let folder_name = folder_entry.file_name().to_string_lossy().to_string(); + // Grey text for starting folder + log::info!("\x1b[90mStarting folder {folder_name}\x1b[0m"); + // Create folder in database let folder = Folder { id: None, - name: folder_name, + name: folder_name.clone(), parent_id: parent_folder, }; @@ -152,6 +157,9 @@ fn generate_database_from_files_internal( // Recursively process this folder's contents (depth-first) let child_path = folder_entry.path(); generate_database_from_files_internal(child_path.to_str().unwrap_or(""), folder_id, con)?; + + // Cyan text for finished folder + log::info!("\x1b[36mFinished folder {folder_name}\x1b[0m"); } // Then process files at this level @@ -160,7 +168,7 @@ fn generate_database_from_files_internal( let file_path = file_entry.path(); // Get file size - let file_size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or(0); + let file_size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or_default(); // Determine file type let file_type: FileTypes = determine_file_type(&file_name); @@ -181,6 +189,9 @@ fn generate_database_from_files_internal( if let Some(folder_id) = parent_folder { folder_repository::link_folder_to_file(file_id, folder_id, con)?; } + + // Queue file for icon generation + queue::publish_message("icon_gen", &file_id.to_string()); } Ok(()) From e6dc544e55c19a14540feac3d4843e3b3a767efe Mon Sep 17 00:00:00 2001 From: ploiu Date: Wed, 26 Nov 2025 01:16:45 +0000 Subject: [PATCH 6/7] some cleanup --- src/repository/mod.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 5c40fa2..382615f 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::fs::{self, DirEntry}; use std::path::Path; #[cfg(not(test))] @@ -105,10 +105,8 @@ pub fn generate_database_from_files(parent_folder: Option, con: &Connection return Ok(()); } - // Magenta text for starting job log::info!("\x1b[35mGenerating database from file system\x1b[0m"); let result = generate_database_from_files_internal(&base_path, None, con); - // Green text for finished job log::info!("\x1b[32mFinished generating database from file system\x1b[0m"); result } @@ -141,7 +139,6 @@ fn generate_database_from_files_internal( for folder_entry in folders { let folder_name = folder_entry.file_name().to_string_lossy().to_string(); - // Grey text for starting folder log::info!("\x1b[90mStarting folder {folder_name}\x1b[0m"); // Create folder in database @@ -156,9 +153,12 @@ fn generate_database_from_files_internal( // Recursively process this folder's contents (depth-first) let child_path = folder_entry.path(); - generate_database_from_files_internal(child_path.to_str().unwrap_or(""), folder_id, con)?; + generate_database_from_files_internal( + child_path.to_str().unwrap_or_default(), + folder_id, + con, + )?; - // Cyan text for finished folder log::info!("\x1b[36mFinished folder {folder_name}\x1b[0m"); } @@ -168,7 +168,9 @@ fn generate_database_from_files_internal( let file_path = file_entry.path(); // Get file size - let file_size = fs::metadata(&file_path).map(|m| m.len()).unwrap_or_default(); + let file_size = fs::metadata(&file_path) + .map(|m| m.len()) + .unwrap_or_default(); // Determine file type let file_type: FileTypes = determine_file_type(&file_name); From a08184be52532de0ddbe8ba4ec589b3bc7fb6c79 Mon Sep 17 00:00:00 2001 From: ploiu Date: Wed, 26 Nov 2025 01:17:17 +0000 Subject: [PATCH 7/7] cargo --- src/repository/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/repository/mod.rs b/src/repository/mod.rs index 382615f..a5021d1 100644 --- a/src/repository/mod.rs +++ b/src/repository/mod.rs @@ -1,4 +1,4 @@ -use std::fs::{self, DirEntry}; +use std::fs::{self}; use std::path::Path; #[cfg(not(test))]