diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 12a6830..d2a04f8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1565,6 +1565,7 @@ dependencies = [ "tauri-plugin-store", "tokio", "uhlc", + "uuid", ] [[package]] @@ -5019,12 +5020,14 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ "getrandom 0.3.2", + "js-sys", "serde", + "wasm-bindgen", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d720ac5..cd69a9e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -45,6 +45,7 @@ tauri-plugin-http = "2.5" tauri-plugin-notification = "2.3" tauri-plugin-persisted-scope = "2.0.0" tauri-plugin-android-fs = "9.5.0" +uuid = { version = "1.17.0", features = ["v4"] } #tauri-plugin-sql = { version = "2", features = ["sqlite"] } diff --git a/src-tauri/database/schemas/crdt.ts b/src-tauri/database/schemas/crdt.ts index c714fce..4fcb7ef 100644 --- a/src-tauri/database/schemas/crdt.ts +++ b/src-tauri/database/schemas/crdt.ts @@ -6,8 +6,8 @@ export const haexCrdtMessages = sqliteTable('haex_crdt_messages', { row_pks: text({ mode: 'json' }), op_type: text({ enum: ['INSERT', 'UPDATE', 'DELETE'] }), column_name: text(), - new_value: blob(), - old_value: blob(), + new_value: text({ mode: 'json' }), + old_value: text({ mode: 'json' }), }) export type InsertHaexCrdtMessages = typeof haexCrdtMessages.$inferInsert export type SelectHaexCrdtMessages = typeof haexCrdtMessages.$inferSelect @@ -19,3 +19,9 @@ export const haexCrdtSnapshots = sqliteTable('haex_crdt_snapshots', { location_url: text(), file_size_bytes: integer(), }) + +export const haexCrdtSettings = sqliteTable('haex_crdt_settings', { + id: text().primaryKey(), + type: text({ enum: ['hlc_timestamp'] }).unique(), + value: text(), +}) diff --git a/src-tauri/src/crdt/hlc.rs b/src-tauri/src/crdt/hlc.rs new file mode 100644 index 0000000..382f1bc --- /dev/null +++ b/src-tauri/src/crdt/hlc.rs @@ -0,0 +1,67 @@ +use rusqlite::{params, Connection, Result}; +use std::sync::{Arc, Mutex}; +use uhlc::{Timestamp, HLC}; +use uuid::Uuid; + +const HLC_SETTING_TYPE: &str = "hlc_timestamp"; + +pub const GET_HLC_FUNCTION: &str = "get_hlc_timestamp"; +pub const CRDT_SETTINGS_TABLE: &str = "haex_crdt_settings"; +pub struct HlcService(pub Arc>); + +pub fn setup_hlc(conn: &mut Connection) -> Result<()> { + // 1. Lade den letzten HLC-Zustand oder erstelle einen neuen. + let hlc = conn + .query_row( + "SELECT value FROM {CRDT_SETTINGS_TABLE} meta WHERE type = ?1", + params![HLC_SETTING_TYPE], + |row| { + let state_str: String = row.get(0)?; + let timestamp = Timestamp::from_str(&state_str) + .map_err(|_| rusqlite::Error::ExecuteReturnedResults)?; // Konvertiere den Fehler + Ok(HLC::new(timestamp)) + }, + ) + .unwrap_or_else(|_| HLC::default()); // Bei Fehler (z.B. nicht gefunden) -> neuen HLC erstellen. + + let hlc_arc = Arc::new(Mutex::new(hlc)); + + // 2. Erstelle eine Klon für die SQL-Funktion und speichere den Zustand bei jeder neuen Timestamp-Generierung. + let hlc_clone = hlc_arc.clone(); + let db_conn_arc = Arc::new(Mutex::new(conn.try_clone()?)); + + conn.create_scalar_function( + GET_HLC_FUNCTION, + 0, + rusqlite::functions::FunctionFlags::SQLITE_UTF8 + | rusqlite::functions::FunctionFlags::SQLITE_DETERMINISTIC, + move |_| { + let mut hlc = hlc_clone.lock().unwrap(); + let new_timestamp = hlc.new_timestamp(); + let timestamp_str = new_timestamp.to_string(); + + // 3. Speichere den neuen Zustand sofort zurück in die DB. + // UPSERT-Logik: Ersetze den Wert, falls der Schlüssel existiert, sonst füge ihn ein. + let db_conn = db_conn_arc.lock().unwrap(); + db_conn + .execute( + "INSERT INTO {CRDT_SETTINGS_TABLE} (id, type, value) VALUES (?1, ?2, ?3) + ON CONFLICT(type) DO UPDATE SET value = excluded.value", + params![ + Uuid::new_v4().to_string(), // Generiere eine neue ID für den Fall eines INSERTs + HLC_SETTING_TYPE, + ×tamp_str + ], + ) + .expect("HLC state could not be persisted."); // In Prod sollte hier ein besseres Error-Handling hin. + + Ok(timestamp_str) + }, + )?; + + // Hinweis: Den HLC-Service im Tauri-State zu managen ist nicht mehr zwingend, + // da die SQL-Funktion nun alles Notwendige über geklonte Arcs erhält. + // Falls du ihn dennoch für andere Commands brauchst, kannst du ihn im State speichern. + + Ok(()) +} diff --git a/src-tauri/src/crdt/mod.rs b/src-tauri/src/crdt/mod.rs new file mode 100644 index 0000000..01843bd --- /dev/null +++ b/src-tauri/src/crdt/mod.rs @@ -0,0 +1,4 @@ +pub mod hlc; +pub mod log; +pub mod proxy; +pub mod trigger; diff --git a/src-tauri/src/crdt/proxy.rs b/src-tauri/src/crdt/proxy.rs index 59f4e53..1bab7be 100644 --- a/src-tauri/src/crdt/proxy.rs +++ b/src-tauri/src/crdt/proxy.rs @@ -1,7 +1,6 @@ // In src-tauri/src/sql_proxy.rs use rusqlite::Connection; -use sqlparser::ast::Statement; use sqlparser::ast::{ColumnDef, DataType, Expr, Ident, Query, Statement, TableWithJoins, Value}; use sqlparser::dialect::SQLiteDialect; use sqlparser::parser::Parser; @@ -9,11 +8,9 @@ use sqlparser::visit_mut::{self, VisitorMut}; use std::ops::ControlFlow; // Der Name der Tombstone-Spalte als Konstante, um "Magic Strings" zu vermeiden. -pub const TOMBSTONE_COLUMN_NAME: &str = "tombstone"; -const EXCLUDED_TABLES: &[&str] = &["crdt_log"]; +pub const TOMBSTONE_COLUMN_NAME: &str = "haex_tombstone"; +const EXCLUDED_TABLES: &[&str] = &["haex_crdt_log"]; -// Die Hauptstruktur unseres Proxys. -// Sie ist zustandslos, da wir uns gegen einen Schema-Cache entschieden haben. pub struct SqlProxy; impl SqlProxy { diff --git a/src-tauri/src/crdt/trigger.rs b/src-tauri/src/crdt/trigger.rs index e8c2719..531ff76 100644 --- a/src-tauri/src/crdt/trigger.rs +++ b/src-tauri/src/crdt/trigger.rs @@ -1,8 +1,10 @@ // In src-tauri/src/trigger_manager.rs -> impl<'a> TriggerManager<'a> // In einem neuen Modul, z.B. src-tauri/src/trigger_manager.rs -use crate::sql_proxy::ColumnInfo; -use rusqlite::{Result, Transaction}; +use crate::crdt::proxy::ColumnInfo; +use rusqlite::{params, Connection, Result, Transaction}; +use std::sync::{Arc, Mutex}; +use tauri::AppHandle; pub struct TriggerManager<'a> { tx: &'a Transaction<'a>, @@ -94,7 +96,7 @@ impl<'a> TriggerManager<'a> { )).collect::>().join("\n"); // Erstellt die Logik für den Soft-Delete - let delete_logic = format!( + let soft_delete_logic = format!( r#" -- Protokolliere den Soft-Delete INSERT INTO crdt_log (hlc_timestamp, op_type, table_name, row_pk) @@ -117,7 +119,7 @@ impl<'a> TriggerManager<'a> { FOR EACH ROW BEGIN {column_updates} - {delete_logic} + {soft_delete_logic} END;" ) } diff --git a/src-tauri/src/database/core.rs b/src-tauri/src/database/core.rs index 8d6dca9..abe5360 100644 --- a/src-tauri/src/database/core.rs +++ b/src-tauri/src/database/core.rs @@ -1,4 +1,5 @@ // database/core.rs +use crate::crdt::hlc; use crate::database::DbConnection; use base64::{engine::general_purpose::STANDARD, Engine as _}; use rusqlite::{ @@ -6,8 +7,6 @@ use rusqlite::{ Connection, OpenFlags, ToSql, }; use serde_json::Value as JsonValue; -use std::fs; -use std::path::Path; use tauri::State; // --- Hilfsfunktion: Konvertiert JSON Value zu etwas, das rusqlite versteht --- // Diese Funktion ist etwas knifflig wegen Ownership und Lifetimes. @@ -39,8 +38,6 @@ fn json_to_rusqlite_value(json_val: &JsonValue) -> Result } } -// --- Tauri Command für INSERT/UPDATE/DELETE --- -#[tauri::command] pub async fn execute( sql: String, params: Vec, @@ -67,7 +64,6 @@ pub async fn execute( Ok(affected_rows) } -#[tauri::command] pub async fn select( sql: String, params: Vec, @@ -194,32 +190,6 @@ pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result, T: AsRef>( - source_path: S, - target_path: T, -) -> Result<(), String> { - let source = source_path.as_ref(); - let target = target_path.as_ref(); - - // Check if source file exists - if !source.exists() { - return Err(format!("Source file '{}' does not exist", source.display())); - } - - // Check if source is a file (not a directory) - if !source.is_file() { - return Err(format!("Source '{}' is not a file", source.display())); - } - - // Copy the file and preserve metadata (permissions, timestamps) - fs::copy(source, target) - .map(|_| ()) - .map_err(|e| format!("Failed to copy file: {}", e))?; - - Ok(()) -} - // Hilfsfunktionen für SQL-Parsing pub fn extract_tables_from_query(query: &sqlparser::ast::Query) -> Vec { let mut tables = Vec::new(); diff --git a/src-tauri/src/database/mod.rs b/src-tauri/src/database/mod.rs index 56f9fd1..46fc964 100644 --- a/src-tauri/src/database/mod.rs +++ b/src-tauri/src/database/mod.rs @@ -177,7 +177,6 @@ pub fn create_encrypted_database( Ok(format!("Verschlüsselte CRDT-Datenbank erstellt",)) } -use tauri_plugin_dialog::{Dialog, DialogExt, MessageDialogKind}; #[tauri::command] pub fn open_encrypted_database( app_handle: AppHandle, @@ -205,312 +204,6 @@ pub fn open_encrypted_database( Ok(format!("success")) } -fn prepare_temporary_asset_db( - app_handle: &AppHandle, - asset_name: &str, - temp_base_dir: BaseDirectory, -) -> Result { - println!("Lade Asset '{}' aus dem App-Bundle...", asset_name); - - //.resolve("vault.db", BaseDirectory::Resource) - let asset_bytes = app_handle - .asset_resolver() - .get(asset_name.to_owned()) - .ok_or_else(|| format!("Asset '{}' wurde nicht im Bundle gefunden.", asset_name))? - .bytes() - .to_vec(); - - println!( - "Asset '{}' erfolgreich geladen ({} bytes).", - asset_name, - asset_bytes.len() - ); - - let temp_db_filename = format!("temp_unencrypted_{}", asset_name); - let temp_db_path = app_handle - .path() - .resolve(&temp_db_filename, temp_base_dir) - .map_err(|e| { - format!( - "Fehler beim Auflösen des Pfads für die temporäre DB '{}': {}", - temp_db_filename, e - ) - })?; - - println!( - "Temporärer Pfad für unverschlüsselte DB: {}", - temp_db_path.display() - ); - - if let Some(parent) = temp_db_path.parent() { - if !parent.exists() { - fs::create_dir_all(parent).map_err(|e| { - format!( - "Fehler beim Erstellen des temporären Verzeichnisses '{}': {}", - parent.display(), - e - ) - })?; - println!("Temporäres Verzeichnis '{}' erstellt.", parent.display()); - } - } - - if temp_db_path.exists() { - fs::remove_file(&temp_db_path).map_err(|e| { - format!( - "Fehler beim Löschen der alten temporären DB '{}': {}", - temp_db_path.display(), - e - ) - })?; - println!("Alte temporäre DB '{}' gelöscht.", temp_db_path.display()); - } - - fs::write(&temp_db_path, &asset_bytes).map_err(|e| { - format!( - "Fehler beim Schreiben der Asset-DB nach '{}': {}", - temp_db_path.display(), - e - ) - })?; - println!( - "Asset-DB erfolgreich nach '{}' geschrieben.", - temp_db_path.display() - ); - - Ok(temp_db_path) -} - -/// Hilfsfunktion: Verschlüsselt eine Quelldatenbank in eine Zieldatenbank. -fn encrypt_database_from_source( - unencrypted_source_path: &Path, - target_encrypted_path_str: &str, - key: &str, -) -> Result<(), String> { - println!( - "Öffne temporäre Quelldatenbank '{}'...", - unencrypted_source_path.display() - ); - let source_conn = Connection::open(unencrypted_source_path).map_err(|e| { - format!( - "Fehler beim Öffnen der Quelldatenbank '{}': {}", - unencrypted_source_path.display(), - e - ) - })?; - println!( - "Verbindung zur Quelldatenbank '{}' geöffnet.", - unencrypted_source_path.display() - ); - - let final_encrypted_db_path = PathBuf::from(target_encrypted_path_str); - println!( - "Zielpfad für verschlüsselte DB: {}", - final_encrypted_db_path.display() - ); - - if let Some(parent) = final_encrypted_db_path.parent() { - if !parent.exists() { - fs::create_dir_all(parent).map_err(|e| { - format!( - "Fehler beim Erstellen des Zielverzeichnisses '{}': {}", - parent.display(), - e - ) - })?; - println!("Zielverzeichnis '{}' erstellt.", parent.display()); - } - } - if final_encrypted_db_path.exists() { - fs::remove_file(&final_encrypted_db_path).map_err(|e| { - format!( - "Fehler beim Löschen der alten verschlüsselten DB '{}': {}", - final_encrypted_db_path.display(), - e - ) - })?; - println!( - "Alte verschlüsselte DB '{}' gelöscht.", - final_encrypted_db_path.display() - ); - } - - let attach_path_str = final_encrypted_db_path.to_str().ok_or_else(|| { - format!( - "Ungültiger UTF-8 Pfad für ATTACH: {}", - final_encrypted_db_path.display() - ) - })?; - - println!( - "Hänge neue verschlüsselte DB an: '{}' mit KEY '{}'", - attach_path_str, key - ); - source_conn - .execute( - "ATTACH DATABASE ?1 AS encrypted_vault KEY ?2;", - &[attach_path_str, key], - ) - .map_err(|e| format!("Fehler bei ATTACH DATABASE an '{}': {}", attach_path_str, e))?; - println!("Verschlüsselte DB 'encrypted_vault' erfolgreich angehängt."); - - println!("Exportiere Daten von 'main' (Quelle) nach 'encrypted_vault'..."); - if let Err(e) = source_conn.execute("SELECT sqlcipher_export('encrypted_vault');", []) { - eprintln!("!!! FEHLER während sqlcipher_export: {}", e); - source_conn - .execute("DETACH DATABASE encrypted_vault;", []) - .ok(); // Best-effort cleanup - return Err(format!("Fehler bei sqlcipher_export: {}", e)); - } - println!("SQLCipher Export nach 'encrypted_vault' erfolgreich."); - - println!("Löse 'encrypted_vault'..."); - source_conn - .execute("DETACH DATABASE encrypted_vault;", []) - .map_err(|e| format!("Fehler bei DETACH DATABASE 'encrypted_vault': {}", e))?; - println!("'encrypted_vault' erfolgreich gelöst."); - - // Verbindung zur Quelldatenbank wird hier durch drop(source_conn) geschlossen. - Ok(()) -} - -/// Hilfsfunktion: Öffnet eine verschlüsselte Datenbank und verifiziert sie. -/// Gibt die geöffnete und verifizierte Verbindung zurück. -fn open_and_verify_encrypted_db(db_path: &Path, key: &str) -> Result { - println!( - "Öffne verschlüsselte DB '{}' zur Überprüfung...", - db_path.display() - ); - let conn = Connection::open(db_path).map_err(|e| { - format!( - "Fehler beim Öffnen der verschlüsselten DB '{}' für Check: {}", - db_path.display(), - e - ) - })?; - - conn.pragma_update(None, "key", key).map_err(|e| { - format!( - "Fehler beim Setzen des PRAGMA key für DB '{}': {}", - db_path.display(), - e - ) - })?; - println!("PRAGMA key für DB '{}' gesetzt.", db_path.display()); - - println!("Prüfe SQLCipher-Version auf DB '{}'...", db_path.display()); - match conn.query_row("PRAGMA cipher_version;", [], |row| row.get::<_, String>(0)) { - Ok(version) => { - println!( - "SQLCipher ist aktiv auf DB '{}'! Version: {}", - db_path.display(), - version - ); - match conn.query_row( - "SELECT count(*) FROM sqlite_master WHERE type='table';", - [], - |row| row.get::<_, i32>(0), - ) { - Ok(count) => println!( - "Testabfrage erfolgreich: {} Tabelle(n) in DB '{}' gefunden.", - count, - db_path.display() - ), - Err(e) => { - eprintln!( - "Fehler bei Testabfrage auf verschlüsselter DB '{}': {}", - db_path.display(), - e - ); - return Err(format!( - "Testabfrage auf verschlüsselter DB '{}' fehlgeschlagen: {}", - db_path.display(), - e - )); - } - } - } - Err(e) => { - eprintln!( - "FEHLER: SQLCipher scheint NICHT aktiv zu sein auf DB '{}'!", - db_path.display() - ); - eprintln!("'PRAGMA cipher_version;' schlug fehl: {}", e); - return Err(format!( - "SQLCipher Aktivitätscheck für DB '{}' fehlgeschlagen: {}", - db_path.display(), - e - )); - } - } - Ok(conn) -} - -/// Hauptfunktion: Erstellt eine verschlüsselte Datenbank aus einem gebündelten Asset. -#[tauri::command] -pub fn create_encrypted_database_new( - app_handle: AppHandle, - path: String, - key: String, - state: State<'_, DbConnection>, -) -> Result { - let asset_name = "database/vault.db"; - let temp_db_path: PathBuf; // Muss deklariert werden, um im Fehlerfall aufgeräumt werden zu können - - // Schritt 1: Asset vorbereiten - match prepare_temporary_asset_db(&app_handle, &asset_name, BaseDirectory::Resource) { - Ok(path) => temp_db_path = path, - Err(e) => return Err(e), - } - - // Schritt 2: Datenbank verschlüsseln - // Wir geben einen String-Slice für path, da die Funktion das erwartet. - if let Err(e) = encrypt_database_from_source(&temp_db_path, &path, &key) { - // Versuche, die temporäre Datei auch im Fehlerfall zu löschen - let _ = fs::remove_file(&temp_db_path); // Fehler beim Löschen hier ignorieren - return Err(e); - } - - // Schritt 3: Temporäre Datei aufräumen - if let Err(e) = fs::remove_file(&temp_db_path) { - // Logge den Fehler, aber fahre fort, da die verschlüsselte DB erstellt wurde - eprintln!("Warnung: Fehler beim Löschen der temporären DB '{}': {}. Die verschlüsselte DB wurde jedoch erstellt.", temp_db_path.display(), e); - } else { - println!( - "Temporäre DB '{}' erfolgreich gelöscht.", - temp_db_path.display() - ); - } - println!("Datenbank erfolgreich nach '{}' verschlüsselt.", path); - - // Schritt 4: Neu erstellte verschlüsselte Datenbank öffnen und verifizieren - let final_encrypted_db_path = PathBuf::from(path.clone()); // Klonen, da String für Pfad benötigt wird - let final_conn = match open_and_verify_encrypted_db(&final_encrypted_db_path, &key) { - Ok(conn) => conn, - Err(e) => { - // Wenn das Öffnen/Verifizieren fehlschlägt, existiert die Datei vielleicht, ist aber unbrauchbar. - // Je nach Strategie könnte man hier die `final_encrypted_db_path` löschen. - return Err(e); - } - }; - - // Schritt 5: Datenbankverbindung im State aktualisieren - println!( - "Aktualisiere DB-Verbindung im State mit '{}'", - final_encrypted_db_path.display() - ); - let mut db_state_lock = state - .0 - .lock() - .map_err(|e| format!("Mutex-Fehler beim Sperren des DB-Status: {}", e.to_string()))?; - *db_state_lock = Some(final_conn); - - Ok(format!( - "Verschlüsselte Datenbank erfolgreich erstellt, geprüft und im State gespeichert unter: {}", - final_encrypted_db_path.display() - )) -} - fn get_target_triple() -> Result { let target_triple = if cfg!(target_os = "linux") { if cfg!(target_arch = "x86_64") { @@ -569,23 +262,6 @@ fn get_target_triple() -> Result { Ok(target_triple) } -fn get_crsqlite_path(app_handle: AppHandle) -> Result { - // Laden der cr-sqlite Erweiterung - let target_triple = get_target_triple()?; - - println!("target_triple: {}", target_triple); - - let crsqlite_path = app_handle - .path() - .resource_dir() - .map_err(|e| format!("Fehler beim Ermitteln des Ressourcenverzeichnisses: {}", e))? - .join(format!("crsqlite-{}", target_triple)); - - println!("crsqlite_path: {}", crsqlite_path.display()); - Ok(crsqlite_path) -} - -#[tauri::command] pub fn get_hlc_timestamp(state: tauri::State) -> String { let hlc = state.0.lock().unwrap(); hlc.new_timestamp().to_string() @@ -604,16 +280,6 @@ pub fn update_hlc_from_remote( .map_err(|e| format!("HLC update failed: {:?}", e)) } -#[derive(Debug, Clone)] -struct SqlTableInfo { - cid: u32, - name: String, - r#type: String, - notnull: bool, - dflt_value: Option, - pk: u8, -} - #[tauri::command] pub async fn create_crdt_trigger_for_table( state: &State<'_, DbConnection>, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5afd18c..7a3249a 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,5 +1,6 @@ //mod browser; mod android_storage; +mod crdt; mod database; mod extension; mod models; @@ -44,6 +45,7 @@ pub fn run() { } }) .manage(DbConnection(Arc::new(Mutex::new(None)))) + .manage(database::HlcService(Mutex::new(uhlc::HLC::default()))) .manage(ExtensionState::default()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_fs::init()) @@ -61,7 +63,6 @@ pub fn run() { database::sql_execute, database::sql_select, database::test, - database::get_hlc_timestamp, database::update_hlc_from_remote, extension::copy_directory, extension::database::extension_sql_execute, diff --git a/src/pages/index.vue b/src/pages/index.vue index ef3263a..05668db 100644 --- a/src/pages/index.vue +++ b/src/pages/index.vue @@ -21,9 +21,6 @@ v-model:open="passwordPromptOpen" :path="vaultPath" /> - - Storage Request - res: {{ res }}
{ - try { - res.value = await storage.requestStoragePermission() - res.value += ' wat the fuk' - } catch (error) { - res.value = error - } -}