Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions src/repository/mod.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
use std::fs::{self};
use std::path::Path;

#[cfg(not(test))]
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::queue;
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 {
Expand Down Expand Up @@ -43,16 +51,150 @@ 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::<u64>().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.
///
/// 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` - Should be None to start from root. Any other value is a no-op.
/// * `con` - Database connection
///
/// # Returns
/// * `Result<()>` - Ok if successful, or a rusqlite error
pub fn generate_database_from_files(parent_folder: Option<u32>, 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(());
}

log::info!("\x1b[35mGenerating database from file system\x1b[0m");
let result = generate_database_from_files_internal(&base_path, None, con);
log::info!("\x1b[32mFinished generating database from file system\x1b[0m");
result
}

/// 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<u32>,
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 folders: Vec<_> = entries
.iter()
.filter(|e| e.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
.collect();
let files: Vec<_> = entries
.iter()
.filter(|e| e.file_type().map(|ft| ft.is_file()).unwrap_or(false))
.collect();

// 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();

log::info!("\x1b[90mStarting folder {folder_name}\x1b[0m");

// Create folder in database
let folder = Folder {
id: None,
name: folder_name.clone(),
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_default(),
folder_id,
con,
)?;

log::info!("\x1b[36mFinished folder {folder_name}\x1b[0m");
}

// 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_default();

// 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)?;
}

// Queue file for icon generation
queue::publish_message("icon_gen", &file_id.to_string());
}

Ok(())
}
Loading