added hlc logic

This commit is contained in:
2025-07-11 13:29:34 +02:00
parent 41472e02ad
commit 0c304d7900
11 changed files with 96 additions and 394 deletions

7
src-tauri/Cargo.lock generated
View File

@ -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]]

View File

@ -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"] }

View File

@ -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(),
})

67
src-tauri/src/crdt/hlc.rs Normal file
View File

@ -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<Mutex<HLC>>);
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,
&timestamp_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(())
}

View File

@ -0,0 +1,4 @@
pub mod hlc;
pub mod log;
pub mod proxy;
pub mod trigger;

View File

@ -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 {

View File

@ -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::<Vec<_>>().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;"
)
}

View File

@ -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<RusqliteValue, String>
}
}
// --- Tauri Command für INSERT/UPDATE/DELETE ---
#[tauri::command]
pub async fn execute(
sql: String,
params: Vec<JsonValue>,
@ -67,7 +64,6 @@ pub async fn execute(
Ok(affected_rows)
}
#[tauri::command]
pub async fn select(
sql: String,
params: Vec<JsonValue>,
@ -194,32 +190,6 @@ pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connectio
Ok(conn)
}
/// Kopiert eine Datei von einem Pfad zu einem anderen
pub fn copy_file<S: AsRef<Path>, T: AsRef<Path>>(
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<String> {
let mut tables = Vec::new();

View File

@ -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<Wry>,
asset_name: &str,
temp_base_dir: BaseDirectory,
) -> Result<PathBuf, String> {
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<Connection, String> {
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<Wry>,
path: String,
key: String,
state: State<'_, DbConnection>,
) -> Result<String, String> {
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<String, String> {
let target_triple = if cfg!(target_os = "linux") {
if cfg!(target_arch = "x86_64") {
@ -569,23 +262,6 @@ fn get_target_triple() -> Result<String, String> {
Ok(target_triple)
}
fn get_crsqlite_path(app_handle: AppHandle) -> Result<PathBuf, String> {
// 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<HlcService>) -> 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<String>,
pk: u8,
}
#[tauri::command]
pub async fn create_crdt_trigger_for_table(
state: &State<'_, DbConnection>,

View File

@ -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,

View File

@ -21,9 +21,6 @@
v-model:open="passwordPromptOpen"
:path="vaultPath"
/>
<UiButton @click="requesty()">Storage Request</UiButton>
res: {{ res }}
</div>
<div
@ -94,18 +91,6 @@ const { syncLastVaultsAsync, removeVaultAsync } = useLastVaultStore()
const { lastVaults } = storeToRefs(useLastVaultStore())
await syncLastVaultsAsync()
const res = ref()
const storage = useAndroidStorage()
const requesty = async () => {
try {
res.value = await storage.requestStoragePermission()
res.value += ' wat the fuk'
} catch (error) {
res.value = error
}
}
</script>
<i18n lang="json">