diff --git a/Cargo.lock b/Cargo.lock index e8546f1..8e2ecd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.11" @@ -290,6 +305,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + [[package]] name = "clap" version = "4.4.18" @@ -348,6 +377,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1044,6 +1079,29 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -1798,6 +1856,7 @@ name = "sheet-organizer" version = "0.1.1" dependencies = [ "blake3", + "chrono", "clap", "dotenvy", "env_logger", @@ -2539,6 +2598,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 673a595..adefd28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite", "migrate", "ma tokio = { version = "1", features = ["full"] } blake3 = "1.5.0" dotenvy = "0.15.7" +chrono = "0.4.33" [profile.dev.package.sqlx-macros] opt-level = 3 diff --git a/db-migrations/0_creation.sql b/db-migrations/0_creation.sql index 818af70..b56b5fd 100644 --- a/db-migrations/0_creation.sql +++ b/db-migrations/0_creation.sql @@ -1,2 +1,4 @@ -CREATE TABLE IF NOT EXISTS sheets (id integer primary key autoincrement, name TEXT, path TEXT, file_size INTEGER, file_hash TEXT); +CREATE TABLE IF NOT EXISTS sheets (id integer primary key autoincrement, name TEXT, composer_id integer, path TEXT, file_size INTEGER, file_hash TEXT, last_opened INTEGER); CREATE TABLE IF NOT EXISTS composers (id integer primary key autoincrement, name TEXT); + +CREATE TABLE IF NOT EXISTS orphan_files (id integer primary key autoincrement, path TEXT, file_size INTEGER, file_hash TEXT, last_opened INTEGER); diff --git a/src/database.rs b/src/database.rs index 27228e9..feb7ca5 100644 --- a/src/database.rs +++ b/src/database.rs @@ -1,7 +1,7 @@ use log::debug; use sqlx::{migrate::MigrateDatabase, Sqlite, SqlitePool}; -use crate::sheet::Sheet; +use crate::sheet::{OrphanFile, Sheet}; pub struct Database { connection: SqlitePool, @@ -36,14 +36,16 @@ impl Database { pub async fn _insert_sheet(&self, sheet: Sheet) -> sqlx::Result<()> { sqlx::query( " - INSERT INTO sheets (name, path, file_size, file_hash) - VALUES ($1, $2, $3, $4) + INSERT INTO sheets (name, composer_id, path, file_size, file_hash, last_modified) + VALUES ($1, $2, $3, $4, $5, $6) ", ) .bind(sheet.name) - .bind(sheet.path) - .bind(sheet.file_size) + .bind(sheet.composer_id) + .bind(sheet.path.to_str().unwrap().to_string()) + .bind(sheet.file_size as i32) .bind(sheet.file_hash) + .bind(sheet.last_opened.timestamp()) .execute(&self.connection) .await .map(|_| ()) @@ -51,7 +53,7 @@ impl Database { pub async fn update_sheet_path(&self, sheet: &Sheet) -> sqlx::Result<()> { sqlx::query("UPDATE sheets SET path = $1 WHERE id = $2") - .bind(sheet.path.clone()) + .bind(sheet.path.to_str().unwrap().to_string()) .bind(sheet.id) .execute(&self.connection) .await @@ -63,4 +65,35 @@ impl Database { .fetch_all(&self.connection) .await } + + pub async fn insert_orphan_file(&self, file: OrphanFile) -> sqlx::Result<()> { + sqlx::query( + " + INSERT INTO orphan_files (path, file_size, file_hash, last_modified) + 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.last_opened.timestamp()) + .execute(&self.connection) + .await + .map(|_| ()) + } + + pub async fn update_orphan_file_path(&self, file: &OrphanFile) -> sqlx::Result<()> { + sqlx::query("UPDATE orphan_files SET path = $1 WHERE id = $2") + .bind(file.path.to_str().unwrap().to_string()) + .bind(file.id) + .execute(&self.connection) + .await + .map(|_| ()) + } + + pub async fn fetch_all_orphan_files(&self) -> sqlx::Result> { + sqlx::query_as::<_, OrphanFile>("SELECT * FROM orphan_files") + .fetch_all(&self.connection) + .await + } } diff --git a/src/main.rs b/src/main.rs index 3f8f19d..45a4fb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,7 +87,7 @@ fn validate_sheet_files(sheets: Vec, dir: impl AsRef) -> FileValida .find(|(_, sheet)| sheet.validate_path(&pdf_file).unwrap_or(false)) { let mut sheet = invalidated_sheets.remove(i); - sheet.path = pdf_file.to_str().unwrap().to_string(); + sheet.path = pdf_file; updated_sheets.push(sheet); } else { unassigned_files.push(pdf_file); diff --git a/src/sheet.rs b/src/sheet.rs index 30b79f2..578e6a5 100644 --- a/src/sheet.rs +++ b/src/sheet.rs @@ -1,14 +1,40 @@ -use std::{fs, path::Path}; +use std::{ + fs, + path::{Path, PathBuf}, +}; -#[derive(sqlx::FromRow, Debug, Clone)] +use sqlx::{prelude::*, sqlite::SqliteRow}; +// use sqlx::{FromRow, sqlite::SqliteRow, sqlx::Row}; +use chrono::{DateTime, NaiveDateTime, Utc}; + +#[derive(Debug, Clone)] pub struct Sheet { pub id: i32, pub name: String, - // #[sqlx(from = "String")] - pub path: String, - // #[sqlx(from = "i64")] - pub file_size: i32, + pub composer_id: i32, + pub path: PathBuf, + pub file_size: u64, pub file_hash: String, + pub last_opened: DateTime, +} + +impl FromRow<'_, SqliteRow> for Sheet { + fn from_row(row: &SqliteRow) -> sqlx::Result { + Ok(Self { + id: row.try_get("id")?, + 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_hash: row.try_get("file_hash")?, + last_opened: NaiveDateTime::from_timestamp_opt( + row.try_get::("last_opened")?, + 0, + ) + .unwrap() + .and_utc(), + }) + } } #[derive(sqlx::FromRow)] @@ -21,7 +47,7 @@ impl Sheet { 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 as i32 == self.file_size { + 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 { @@ -32,3 +58,29 @@ impl Sheet { Ok(false) } } + +#[derive(Debug, Clone)] +pub struct OrphanFile { + pub id: i32, + pub path: PathBuf, + pub file_size: u64, + pub file_hash: String, + pub last_opened: DateTime, +} + +impl FromRow<'_, SqliteRow> for OrphanFile { + fn from_row(row: &SqliteRow) -> sqlx::Result { + Ok(Self { + id: row.try_get("id")?, + path: row.try_get::<&str, _>("path")?.into(), + 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")?, + 0, + ) + .unwrap() + .and_utc(), + }) + } +} diff --git a/src/ui/sheet_model.rs b/src/ui/sheet_model.rs index 21c33fe..66680f8 100644 --- a/src/ui/sheet_model.rs +++ b/src/ui/sheet_model.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use gtk::prelude::*; use relm4::prelude::*; @@ -14,10 +14,10 @@ pub enum SheetModelType { } impl SheetModelType { - pub fn get_path(&self) -> &str { + pub fn get_path(&self) -> &Path { match self { - SheetModelType::Sheet { sheet } => &sheet.path, - SheetModelType::Pdf { path } => path.to_str().unwrap(), + SheetModelType::Sheet { sheet } => &sheet.path.as_path(), + SheetModelType::Pdf { path } => path.as_path(), } } } @@ -64,9 +64,7 @@ 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::Pdf { path } => path.file_name().unwrap().to_str().unwrap().to_string(), }; SheetModel {