From 4d5e6f19fcd3313efccc809d211caf534b631f8b Mon Sep 17 00:00:00 2001 From: Julian Mutter Date: Wed, 7 Feb 2024 19:21:53 +0100 Subject: [PATCH] Successfully implement orphan database inserting --- src/database.rs | 10 +++--- src/main.rs | 71 ++++++++++++++++++++++++++++++++++++++----- src/sheet.rs | 47 ++++++++++++++++++++++++---- src/ui/app.rs | 32 +++++++++---------- src/ui/sheet_model.rs | 14 ++++++--- 5 files changed, 135 insertions(+), 39 deletions(-) diff --git a/src/database.rs b/src/database.rs index feb7ca5..efd16c5 100644 --- a/src/database.rs +++ b/src/database.rs @@ -36,7 +36,7 @@ impl Database { pub async fn _insert_sheet(&self, sheet: Sheet) -> sqlx::Result<()> { sqlx::query( " - INSERT INTO sheets (name, composer_id, path, file_size, file_hash, last_modified) + INSERT INTO sheets (name, composer_id, path, file_size, file_hash, last_opened) VALUES ($1, $2, $3, $4, $5, $6) ", ) @@ -66,20 +66,20 @@ impl Database { .await } - pub async fn insert_orphan_file(&self, file: OrphanFile) -> sqlx::Result<()> { + pub async fn insert_orphan_file(&self, file: &OrphanFile) -> sqlx::Result { sqlx::query( " - INSERT INTO orphan_files (path, file_size, file_hash, last_modified) + INSERT INTO orphan_files (path, file_size, file_hash, last_opened) VALUES ($1, $2, $3, $4) ", ) .bind(file.path.to_str().unwrap().to_string()) .bind(file.file_size as i32) - .bind(file.file_hash) + .bind(file.file_hash.clone()) .bind(file.last_opened.timestamp()) .execute(&self.connection) .await - .map(|_| ()) + .map(|result| result.last_insert_rowid()) } pub async fn update_orphan_file_path(&self, file: &OrphanFile) -> sqlx::Result<()> { diff --git a/src/main.rs b/src/main.rs index 45a4fb2..99890aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use database::Database; use env_logger::Env; use log::{debug, error}; use relm4::RelmApp; -use sheet::Sheet; +use sheet::{OrphanFile, Sheet}; use walkdir::WalkDir; use crate::ui::app::AppModel; @@ -35,17 +35,41 @@ async fn main() { let database = Database::setup("./testdb.sqlite").await.unwrap(); // database.insert_sheet(Sheet::new_debug()).await.unwrap(); let sheets = database.fetch_all_sheets().await.unwrap(); + let orphan_files = database.fetch_all_orphan_files().await.unwrap(); debug!("Validating sheets from database..."); - let validation_result = validate_sheet_files(sheets, &cli.directory); - debug!("{}", validation_result.get_stats()); + let mut validation_result = validate_sheet_files(sheets, orphan_files, &cli.directory); + debug!("{}", validation_result.get_stats()); // TODO: handle invalidated files for updated in validation_result.updated_sheets.iter() { database.update_sheet_path(updated).await.unwrap(); } + for updated in validation_result.updated_orphan_files.iter() { + database.update_orphan_file_path(updated).await.unwrap(); + } + + let mut orphans = validation_result.validated_orphan_files; + debug!("Inserting unassigned files into orphan table..."); + for unassigned in validation_result.unassigned_files { + let mut orphan = OrphanFile::try_from(unassigned).unwrap(); + let id = database.insert_orphan_file(&orphan).await.unwrap(); + orphan.id = id; + orphans.push(orphan); + } + + let mut sheets = validation_result.validated_sheets; + sheets.append(&mut validation_result.updated_sheets); + + let sheets_and_orphans = SheetsAndOrphans { sheets, orphans }; let app = RelmApp::new("de.frajul.sheet-organizer"); // Pass empty command line args to allow my own parsing - app.with_args(Vec::new()).run::(validation_result); + app.with_args(Vec::new()) + .run::(sheets_and_orphans); +} + +pub struct SheetsAndOrphans { + sheets: Vec, + orphans: Vec, } pub struct FileValidationResult { @@ -53,21 +77,39 @@ pub struct FileValidationResult { invalidated_sheets: Vec, updated_sheets: Vec, + validated_orphan_files: Vec, + invalidated_orphan_files: Vec, + updated_orphan_files: Vec, + unassigned_files: Vec, } impl FileValidationResult { fn get_stats(&self) -> String { - format!("Validated sheets: {}\nInvalidated sheets: {}\nUpdated sheets: {}\nUnassigned files: {}", self.validated_sheets.len(), self.invalidated_sheets.len(), self.updated_sheets.len(), self.unassigned_files.len()) + format!("Validated sheets: {}\nInvalidated sheets: {}\nUpdated sheets: {}\nValidated orphan_files: {}\nInvalidated orphan_files: {}\nUpdated orphan_files: {}\nUnassigned files: {}", + self.validated_sheets.len(), self.invalidated_sheets.len(), self.updated_sheets.len(), + self.validated_orphan_files.len(), self.invalidated_orphan_files.len(), self.updated_orphan_files.len(), self.unassigned_files.len()) } } -fn validate_sheet_files(sheets: Vec, dir: impl AsRef) -> FileValidationResult { +fn validate_sheet_files( + sheets: Vec, + orphan_files: Vec, + dir: impl AsRef, +) -> FileValidationResult { + // TODO: fix duplication let (validated_sheets, mut invalidated_sheets): (Vec<_>, Vec<_>) = sheets .into_iter() .partition(|sheet| sheet.validate_path(&sheet.path).unwrap_or(false)); + let (validated_orphan_files, mut invalidated_orphan_files): (Vec<_>, Vec<_>) = + orphan_files.into_iter().partition(|orphan_file| { + orphan_file + .validate_path(&orphan_file.path) + .unwrap_or(false) + }); let mut updated_sheets = Vec::new(); + let mut updated_orphan_files = Vec::new(); let mut unassigned_files = Vec::new(); for pdf_file in WalkDir::new(dir) @@ -89,7 +131,19 @@ fn validate_sheet_files(sheets: Vec, dir: impl AsRef) -> FileValida let mut sheet = invalidated_sheets.remove(i); sheet.path = pdf_file; updated_sheets.push(sheet); - } else { + } else if let Some((i, _)) = invalidated_orphan_files + .iter() + .enumerate() + .find(|(_, orphan_file)| orphan_file.validate_path(&pdf_file).unwrap_or(false)) + { + let mut orphan_file = invalidated_orphan_files.remove(i); + orphan_file.path = pdf_file; + updated_orphan_files.push(orphan_file); + } else if !validated_sheets.iter().any(|sheet| sheet.path == pdf_file) + && !validated_orphan_files + .iter() + .any(|orphan| orphan.path == pdf_file) + { unassigned_files.push(pdf_file); } } @@ -98,6 +152,9 @@ fn validate_sheet_files(sheets: Vec, dir: impl AsRef) -> FileValida validated_sheets, invalidated_sheets, updated_sheets, + validated_orphan_files, + invalidated_orphan_files, + updated_orphan_files, unassigned_files, } } diff --git a/src/sheet.rs b/src/sheet.rs index 578e6a5..b533213 100644 --- a/src/sheet.rs +++ b/src/sheet.rs @@ -9,9 +9,9 @@ use chrono::{DateTime, NaiveDateTime, Utc}; #[derive(Debug, Clone)] pub struct Sheet { - pub id: i32, + pub id: i64, pub name: String, - pub composer_id: i32, + pub composer_id: i64, pub path: PathBuf, pub file_size: u64, pub file_hash: String, @@ -25,7 +25,7 @@ impl FromRow<'_, SqliteRow> for Sheet { name: row.try_get("name")?, composer_id: row.try_get("composer_id")?, path: row.try_get::<&str, _>("path")?.into(), - file_size: row.try_get::("file_size")? as u64, + file_size: row.try_get::("file_size")? as u64, file_hash: row.try_get("file_hash")?, last_opened: NaiveDateTime::from_timestamp_opt( row.try_get::("last_opened")?, @@ -39,7 +39,7 @@ impl FromRow<'_, SqliteRow> for Sheet { #[derive(sqlx::FromRow)] pub struct Composer { - pub id: i32, + pub id: i64, pub name: String, } @@ -59,9 +59,44 @@ impl Sheet { } } +impl OrphanFile { + // TODO: fix duplication + pub fn validate_path(&self, path: impl AsRef) -> std::io::Result { + // First compare file size since it is faster than hashing + let file_size = fs::metadata(path.as_ref())?.len(); + if file_size == self.file_size { + let file_content = fs::read(path.as_ref())?; + let file_hash = blake3::hash(&file_content); + if file_hash.to_string() == self.file_hash { + return Ok(true); + } + } + + Ok(false) + } +} + +impl TryFrom for OrphanFile { + type Error = std::io::Error; + + fn try_from(path: PathBuf) -> Result { + let file_size = fs::metadata(path.as_path())?.len(); + let file_content = fs::read(path.as_path())?; + let file_hash = blake3::hash(&file_content).to_string(); + + Ok(OrphanFile { + id: -1, + path, + file_size, + file_hash, + last_opened: DateTime::default(), + }) + } +} + #[derive(Debug, Clone)] pub struct OrphanFile { - pub id: i32, + pub id: i64, pub path: PathBuf, pub file_size: u64, pub file_hash: String, @@ -73,7 +108,7 @@ impl FromRow<'_, SqliteRow> for OrphanFile { Ok(Self { id: row.try_get("id")?, path: row.try_get::<&str, _>("path")?.into(), - file_size: row.try_get::("file_size")? as u64, + file_size: row.try_get::("file_size")? as u64, file_hash: row.try_get("file_hash")?, last_opened: NaiveDateTime::from_timestamp_opt( row.try_get::("last_opened")?, diff --git a/src/ui/app.rs b/src/ui/app.rs index f732921..13333ba 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -1,11 +1,9 @@ - - use gtk::prelude::*; -use relm4::{prelude::*}; +use relm4::prelude::*; use crate::{ ui::{mcdu::McduOutput, sheet_model::SheetModelType}, - FileValidationResult, + FileValidationResult, SheetsAndOrphans, }; use super::{ @@ -30,7 +28,7 @@ pub enum AppInput { impl SimpleComponent for AppModel { type Input = AppInput; type Output = (); - type Init = FileValidationResult; + type Init = SheetsAndOrphans; view! { #[root] @@ -69,7 +67,7 @@ impl SimpleComponent for AppModel { } fn init( - file_validation_result: Self::Init, + sheets_and_orphans: Self::Init, window: &Self::Root, sender: ComponentSender, ) -> relm4::ComponentParts { @@ -81,22 +79,22 @@ impl SimpleComponent for AppModel { McduOutput::SearchStarted(query) => AppInput::SearchStarted(query), }); - let new_files: Vec = file_validation_result - .unassigned_files + let new_files: Vec = sheets_and_orphans + .orphans .iter() - .map(|file| SheetModelType::Pdf { - path: file.to_path_buf(), + .map(|orphan| SheetModelType::Orphan { + orphan: orphan.clone(), }) .collect(); - let new_files_clone_iter = file_validation_result - .unassigned_files - .into_iter() - .map(|file| SheetModelType::Pdf { path: file }); - let sheets_and_files: Vec = file_validation_result - .validated_sheets + let new_files_clone_iter = sheets_and_orphans + .orphans + .into_iter() + .map(|orphan| SheetModelType::Orphan { orphan }); + + let sheets_and_files: Vec = sheets_and_orphans + .sheets .into_iter() - .chain(file_validation_result.updated_sheets) .map(|sheet| SheetModelType::Sheet { sheet }) .chain(new_files_clone_iter) .collect(); diff --git a/src/ui/sheet_model.rs b/src/ui/sheet_model.rs index 66680f8..c2ca768 100644 --- a/src/ui/sheet_model.rs +++ b/src/ui/sheet_model.rs @@ -3,21 +3,21 @@ use std::path::{Path, PathBuf}; use gtk::prelude::*; use relm4::prelude::*; -use crate::sheet::Sheet; +use crate::sheet::{OrphanFile, Sheet}; use super::sheet_listing::SheetListingInput; #[derive(Debug, Clone)] pub enum SheetModelType { Sheet { sheet: Sheet }, - Pdf { path: PathBuf }, + Orphan { orphan: OrphanFile }, } impl SheetModelType { pub fn get_path(&self) -> &Path { match self { SheetModelType::Sheet { sheet } => &sheet.path.as_path(), - SheetModelType::Pdf { path } => path.as_path(), + SheetModelType::Orphan { orphan } => &orphan.path.as_path(), } } } @@ -64,7 +64,13 @@ impl FactoryComponent for SheetModel { fn init_model(value: Self::Init, _index: &DynamicIndex, _sender: FactorySender) -> Self { let label = match &value { SheetModelType::Sheet { sheet } => sheet.name.to_string(), - SheetModelType::Pdf { path } => path.file_name().unwrap().to_str().unwrap().to_string(), + SheetModelType::Orphan { orphan } => orphan + .path + .file_name() + .unwrap() + .to_str() + .unwrap() + .to_string(), }; SheetModel {