zwischenstand

This commit is contained in:
Martin Drechsel
2025-05-06 11:09:56 +02:00
parent 410a885d21
commit b729c8ebbe
40 changed files with 2252 additions and 376 deletions

37
src-tauri/Cargo.lock generated
View File

@ -1090,6 +1090,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "fs_extra"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
[[package]]
name = "futf"
version = "0.1.5"
@ -1528,6 +1534,10 @@ dependencies = [
name = "haex-hub"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"fs_extra",
"mime",
"mime_guess",
"rusqlite",
"serde",
"serde_json",
@ -1639,6 +1649,12 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "http-range"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
[[package]]
name = "httparse"
version = "1.10.1"
@ -2203,6 +2219,16 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.8.5"
@ -3792,9 +3818,9 @@ dependencies = [
[[package]]
name = "sqlparser"
version = "0.55.0"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4521174166bac1ff04fe16ef4524c70144cd29682a45978978ca3d7f4e0be11"
checksum = "e68feb51ffa54fc841e086f58da543facfe3d7ae2a60d69b0a8cbbd30d16ae8d"
dependencies = [
"log",
"recursive",
@ -4031,6 +4057,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
"http-range",
"jni",
"libc",
"log",
@ -4745,6 +4772,12 @@ dependencies = [
"unic-common",
]
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.18"

View File

@ -27,8 +27,12 @@ rusqlite = { version = "0.35.0", features = [
tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlparser = { version = "0.55.0", features = [] }
tauri = { version = "2.5", features = [] }
base64 = "0.22"
mime_guess = "2.0"
mime = "0.3"
fs_extra = "1.3.0"
sqlparser = { version = "0.56.0", features = [] }
tauri = { version = "2.5", features = ["protocol-asset", "custom-protocol"] }
tauri-plugin-dialog = "2.2"
tauri-plugin-fs = "2.2.0"
tauri-plugin-opener = "2.2"

View File

@ -1,28 +1,29 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"fs:allow-read-file",
"fs:allow-resource-read-recursive",
"fs:default",
"http:allow-fetch-send",
"http:allow-fetch",
"http:default",
"opener:allow-open-url",
"opener:default",
"os:default",
"store:default",
"core:window:allow-create",
"core:window:default",
"core:window:allow-get-all-windows",
"core:window:allow-show",
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window",
"core:webview:default",
"core:webview:allow-webview-show"
]
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"dialog:default",
"fs:allow-read-file",
"fs:allow-resource-read-recursive",
"fs:default",
"fs:allow-resource-write-recursive",
"http:allow-fetch-send",
"http:allow-fetch",
"http:default",
"opener:allow-open-url",
"opener:default",
"os:default",
"store:default",
"core:window:allow-create",
"core:window:default",
"core:window:allow-get-all-windows",
"core:window:allow-show",
"core:webview:allow-create-webview",
"core:webview:allow-create-webview-window",
"core:webview:default",
"core:webview:allow-webview-show"
]
}

View File

@ -0,0 +1,23 @@
import { drizzle } from "drizzle-orm/sqlite-proxy"; // Adapter für Query Building ohne direkte Verbindung
import * as schema from "./schemas/vault"; // Importiere alles aus deiner Schema-Datei
// sqlite-proxy benötigt eine (dummy) Ausführungsfunktion als Argument.
// Diese wird in unserem Tauri-Workflow nie aufgerufen, da wir nur .toSQL() verwenden.
// Sie muss aber vorhanden sein, um drizzle() aufrufen zu können.
const dummyExecutor = async (
sql: string,
params: any[],
method: "all" | "run" | "get" | "values"
) => {
console.warn(
`Frontend Drizzle Executor wurde aufgerufen (Methode: ${method}). Das sollte im Tauri-Invoke-Workflow nicht passieren!`
);
// Wir geben leere Ergebnisse zurück, um die Typen zufriedenzustellen, falls es doch aufgerufen wird.
return { rows: [] }; // Für 'run' (z.B. bei INSERT/UPDATE)
};
// Erstelle die Drizzle-Instanz für den SQLite-Dialekt
// Übergib den dummyExecutor und das importierte Schema
export const db = drizzle(dummyExecutor, { schema });
// Exportiere auch alle Schema-Definitionen weiter, damit man alles aus einer Datei importieren kann

View File

@ -0,0 +1,7 @@
CREATE TABLE `testTable` (
`id` text PRIMARY KEY NOT NULL,
`author` text,
`test` text
);
--> statement-breakpoint
ALTER TABLE `haex_extensions` ADD `icon` text;

View File

@ -0,0 +1,223 @@
{
"version": "6",
"dialect": "sqlite",
"id": "6fb5396b-9f87-4fb5-87a2-22d4eecaa11e",
"prevId": "fc5a7c9d-4846-4120-a762-cc2ea00504b9",
"tables": {
"haex_extensions": {
"name": "haex_extensions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"enabled": {
"name": "enabled",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"icon": {
"name": "icon",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"version": {
"name": "version",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"haex_extensions_permissions": {
"name": "haex_extensions_permissions",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"extension_id": {
"name": "extension_id",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"resource": {
"name": "resource",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"operation": {
"name": "operation",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"path": {
"name": "path",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"haex_extensions_permissions_extension_id_resource_operation_path_unique": {
"name": "haex_extensions_permissions_extension_id_resource_operation_path_unique",
"columns": [
"extension_id",
"resource",
"operation",
"path"
],
"isUnique": true
}
},
"foreignKeys": {
"haex_extensions_permissions_extension_id_haex_extensions_id_fk": {
"name": "haex_extensions_permissions_extension_id_haex_extensions_id_fk",
"tableFrom": "haex_extensions_permissions",
"tableTo": "haex_extensions",
"columnsFrom": [
"extension_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"haex_settings": {
"name": "haex_settings",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"key": {
"name": "key",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"value_text": {
"name": "value_text",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"value_json": {
"name": "value_json",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"value_number": {
"name": "value_number",
"type": "numeric",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
},
"testTable": {
"name": "testTable",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"author": {
"name": "author",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"test": {
"name": "test",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"checkConstraints": {}
}
},
"views": {},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View File

@ -8,6 +8,13 @@
"when": 1742903332283,
"tag": "0000_zippy_scourge",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1746281577722,
"tag": "0001_wealthy_thaddeus_ross",
"breakpoints": true
}
]
}

View File

@ -1,51 +1,47 @@
import {
integer,
sqliteTable,
text,
type AnySQLiteColumn,
unique,
numeric,
integer,
numeric,
sqliteTable,
text,
type AnySQLiteColumn,
unique,
} from "drizzle-orm/sqlite-core";
export function generateCreateStatements() {
const tables = [haexSettings, haexExtensions, testTable, haexExtensionsPermissions];
}
export const haexSettings = sqliteTable("haex_settings", {
id: text().primaryKey(),
key: text(),
value_text: text(),
value_json: text({ mode: "json" }),
value_number: numeric(),
id: text().primaryKey(),
key: text(),
value_text: text(),
value_json: text({ mode: "json" }),
value_number: numeric(),
});
export const haexExtensions = sqliteTable("haex_extensions", {
id: text().primaryKey(),
author: text(),
enabled: integer(),
name: text(),
url: text(),
version: text(),
id: text().primaryKey(),
author: text(),
enabled: integer({ mode: "boolean" }),
icon: text(),
name: text(),
url: text(),
version: text(),
});
export const testTable = sqliteTable("testTable", {
id: text().primaryKey(),
author: text(),
test: text(),
id: text().primaryKey(),
author: text(),
test: text(),
});
export const haexExtensionsPermissions = sqliteTable(
"haex_extensions_permissions",
{
id: text().primaryKey(),
extensionId: text("extension_id").references((): AnySQLiteColumn => haexExtensions.id),
resource: text({ enum: ["fs", "http", "database"] }),
operation: text({ enum: ["read", "write", "create"] }),
path: text(),
},
(table) => [unique().on(table.extensionId, table.resource, table.operation, table.path)]
"haex_extensions_permissions",
{
id: text().primaryKey(),
extensionId: text("extension_id").references((): AnySQLiteColumn => haexExtensions.id),
resource: text({ enum: ["fs", "http", "database"] }),
operation: text({ enum: ["read", "write", "create"] }),
path: text(),
},
(table) => [unique().on(table.extensionId, table.resource, table.operation, table.path)]
);
console.log("table", haexExtensionsPermissions.getSQL());
export type InsertHaexSettings = typeof haexSettings.$inferInsert;
export type SelectHaexSettings = typeof haexSettings.$inferSelect;

Binary file not shown.

View File

@ -1,13 +1,74 @@
// database/core.rs
use crate::database::DbConnection;
use rusqlite::{Connection, OpenFlags};
use serde_json::json;
use base64::{engine::general_purpose::STANDARD, Engine as _};
use rusqlite::{
types::{Value as RusqliteValue, ValueRef},
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.
// Eine einfachere Variante ist oft, direkt rusqlite::types::Value zu erstellen.
// Hier ein Beispiel, das owned Values erstellt (braucht evtl. Anpassung je nach rusqlite-Version/Nutzung)
fn json_to_rusqlite_value(json_val: &JsonValue) -> Result<RusqliteValue, String> {
match json_val {
JsonValue::Null => Ok(RusqliteValue::Null),
JsonValue::Bool(b) => Ok(RusqliteValue::Integer(*b as i64)), // SQLite hat keinen BOOLEAN
JsonValue::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(RusqliteValue::Integer(i))
} else if let Some(f) = n.as_f64() {
Ok(RusqliteValue::Real(f))
} else {
Err("Ungültiger Zahlenwert".to_string())
}
}
JsonValue::String(s) => Ok(RusqliteValue::Text(s.clone())),
JsonValue::Array(_) | JsonValue::Object(_) => {
// SQLite kann Arrays/Objects nicht direkt speichern (außer als TEXT/BLOB)
// Konvertiere sie zu JSON-Strings, wenn das gewünscht ist
Ok(RusqliteValue::Text(
serde_json::to_string(json_val).map_err(|e| e.to_string())?,
))
// Oder gib einen Fehler zurück, wenn Arrays/Objekte nicht erlaubt sind
// Err("Arrays oder Objekte werden nicht direkt als Parameter unterstützt".to_string())
}
}
}
// --- Tauri Command für INSERT/UPDATE/DELETE ---
#[tauri::command]
pub async fn execute(
sql: String,
params: Vec<JsonValue>,
state: &State<'_, DbConnection>,
) -> Result<usize, String> {
// Gibt Anzahl betroffener Zeilen zurück
let params_converted: Vec<RusqliteValue> = params
.iter()
.map(json_to_rusqlite_value)
.collect::<Result<Vec<_>, _>>()?;
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
let db_lock = state
.0
.lock()
.map_err(|e| format!("Mutex Lock Fehler: {}", e))?;
let conn = db_lock.as_ref().ok_or("Keine Datenbankverbindung")?;
let affected_rows = conn
.execute(&sql, &params_sql[..])
.map_err(|e| format!("SQL Execute Fehler: {}", e))?;
Ok(affected_rows)
}
/// Führt SQL-Schreiboperationen (INSERT, UPDATE, DELETE, CREATE) ohne Berechtigungsprüfung aus
pub async fn execute(
/* pub async fn execute(
sql: &str,
params: &[String],
state: &State<'_, DbConnection>,
@ -26,14 +87,107 @@ pub async fn execute(
"last_insert_id": last_id
}))
.map_err(|e| format!("JSON-Serialisierungsfehler: {}", e))?)
} */
#[tauri::command]
pub async fn select(
sql: String,
params: Vec<JsonValue>, // Parameter als JSON Values empfangen
state: &State<'_, DbConnection>,
) -> Result<Vec<Vec<JsonValue>>, String> {
// Ergebnis als Vec<RowObject>
// Konvertiere JSON Params zu rusqlite Values für die Abfrage
// Wir sammeln sie als owned Values, da `params_from_iter` Referenzen braucht,
// was mit lokalen Konvertierungen schwierig ist.
let params_converted: Vec<RusqliteValue> = params
.iter()
.map(json_to_rusqlite_value)
.collect::<Result<Vec<_>, _>>()?; // Sammle Ergebnisse, gibt Fehler weiter
// Konvertiere zu Slice von ToSql-Referenzen (erfordert, dass die Values leben)
let params_sql: Vec<&dyn ToSql> = params_converted.iter().map(|v| v as &dyn ToSql).collect();
// Zugriff auf die Verbindung (blockierend, okay für SQLite in vielen Fällen)
let db_lock = state
.0
.lock()
.map_err(|e| format!("Mutex Lock Fehler: {}", e))?;
let conn = db_lock.as_ref().ok_or("Keine Datenbankverbindung")?;
let mut stmt = conn
.prepare(&sql)
.map_err(|e| format!("SQL Prepare Fehler: {}", e))?;
let column_names: Vec<String> = stmt
.column_names()
.into_iter()
.map(|s| s.to_string())
.collect();
let num_columns = column_names.len();
let mut rows = stmt
.query(&params_sql[..])
.map_err(|e| format!("SQL Query Fehler: {}", e))?;
let mut result_vec: Vec<Vec<JsonValue>> = Vec::new();
println!();
println!();
println!();
println!();
while let Some(row) = rows.next().map_err(|e| format!("Row Next Fehler: {}", e))? {
//let mut row_map = HashMap::new();
let mut row_data: Vec<JsonValue> = Vec::with_capacity(num_columns);
for i in 0..num_columns {
let col_name = &column_names[i];
println!(
"--- Processing Column --- Index: {}, Name: '{}'",
i, col_name
);
let value_ref = row
.get_ref(i)
.map_err(|e| format!("Get Ref Fehler Spalte {}: {}", i, e))?;
// Wandle rusqlite ValueRef zurück zu serde_json Value
let json_val = match value_ref {
ValueRef::Null => JsonValue::Null,
ValueRef::Integer(i) => JsonValue::Number(i.into()),
ValueRef::Real(f) => JsonValue::Number(
serde_json::Number::from_f64(f).unwrap_or(serde_json::Number::from(0)),
), // Fallback für NaN/Infinity
ValueRef::Text(t) => {
let s = String::from_utf8_lossy(t).to_string();
// Versuche, als JSON zu parsen, falls es ursprünglich ein Array/Objekt war
//serde_json::from_str(&s).unwrap_or(JsonValue::String(s))
JsonValue::String(s)
}
ValueRef::Blob(b) => {
// BLOBs z.B. als Base64-String zurückgeben
JsonValue::String(STANDARD.encode(b))
}
};
println!(
"new row: name: {} with value: {}",
column_names[i].clone(),
json_val,
);
row_data.push(json_val);
//row_map.insert(column_names[i].clone(), json_val);
}
//result_vec.push(row_map);
result_vec.push(row_data);
}
Ok(result_vec)
}
/// Führt SQL-Leseoperationen (SELECT) ohne Berechtigungsprüfung aus
pub async fn select(
/* pub async fn select(
sql: &str,
params: &[String],
state: &State<'_, DbConnection>,
) -> Result<Vec<Vec<String>>, String> {
) -> Result<Vec<Vec<Option<String>>>, String> {
let db = state.0.lock().map_err(|e| format!("Mutex-Fehler: {}", e))?;
let conn = db.as_ref().ok_or("Keine Datenbankverbindung vorhanden")?;
@ -52,16 +206,20 @@ pub async fn select(
{
let mut row_data = Vec::new();
for i in 0..columns {
let value: String = row
let value = row
.get(i)
.map_err(|e| format!("Datentypfehler in Spalte {}: {}", i, e))?;
row_data.push(value);
}
/* println!(
"Select Row Data: {}",
&row_data.clone().join("").to_string()
); */
result.push(row_data);
}
Ok(result)
}
} */
/// Öffnet und initialisiert eine Datenbank mit Verschlüsselung
pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connection, String> {
@ -75,6 +233,9 @@ pub fn open_and_init_db(path: &str, key: &str, create: bool) -> Result<Connectio
conn.pragma_update(None, "key", key)
.map_err(|e| e.to_string())?;
conn.execute_batch("SELECT count(*) from haex_extensions")
.map_err(|e| e.to_string())?;
Ok(conn)
}

View File

@ -1,30 +1,30 @@
// database/mod.rs
pub mod core;
use rusqlite::{Connection, OpenFlags};
use rusqlite::Connection;
use serde_json::Value as JsonValue;
use std::path::Path;
use std::sync::Mutex;
use tauri::{path::BaseDirectory, AppHandle, Manager, State};
pub struct DbConnection(pub Mutex<Option<Connection>>);
// Öffentliche Funktionen für direkten Datenbankzugriff
#[tauri::command]
pub async fn sql_select(
sql: String,
params: Vec<String>,
params: Vec<JsonValue>,
state: State<'_, DbConnection>,
) -> Result<Vec<Vec<String>>, String> {
core::select(&sql, &params, &state).await
) -> Result<Vec<Vec<JsonValue>>, String> {
core::select(sql, params, &state).await
}
#[tauri::command]
pub async fn sql_execute(
sql: String,
params: Vec<String>,
params: Vec<JsonValue>,
state: State<'_, DbConnection>,
) -> Result<String, String> {
core::execute(&sql, &params, &state).await
) -> Result<usize, String> {
core::execute(sql, params, &state).await
}
/// Erstellt eine verschlüsselte Datenbank
@ -58,7 +58,62 @@ pub fn create_encrypted_database(
}
}
// Neue Datenbank erstellen
//core::copy_file(&resource_path, &path)?;
println!(
"Öffne unverschlüsselte Datenbank: {}",
resource_path.as_path().display()
);
let conn = Connection::open(&resource_path).map_err(|e| {
format!(
"Fehler beim Öffnen der kopierten Datenbank: {}",
e.to_string()
)
})?;
//let conn = Connection::open(&resource_path)?;
println!("Hänge neue, verschlüsselte Datenbank an unter '{}'", &path);
// ATTACH DATABASE 'Dateiname' AS Alias KEY 'Passwort';
conn.execute("ATTACH DATABASE ?1 AS encrypted KEY ?2;", [&path, &key])
.map_err(|e| format!("Fehler bei ATTACH DATABASE: {}", e.to_string()))?;
println!(
"Exportiere Daten von 'main' nach 'encrypted' mit password {} ...",
&key
);
match conn.query_row("SELECT sqlcipher_export('encrypted');", [], |_row| Ok(())) {
Ok(_) => {
println!(">>> sqlcipher_export erfolgreich ausgeführt (Rückgabewert ignoriert).");
}
Err(e) => {
eprintln!("!!! FEHLER während sqlcipher_export: {}", e);
conn.execute("DETACH DATABASE encrypted;", []).ok(); // Versuche zu detachen
return Err(e.to_string()); // Gib den Fehler zurück
}
}
// sqlcipher_export('Alias') kopiert Schema und Daten von 'main' zur Alias-DB
/* conn.execute("SELECT sqlcipher_export('encrypted');", [])
.map_err(|e| {
format!(
"Fehler bei SELECT sqlcipher_export('encrypted'): {}",
e.to_string()
)
})?; */
println!("Löse die verschlüsselte Datenbank vom Handle...");
conn.execute("DETACH DATABASE encrypted;", [])
.map_err(|e| format!("Fehler bei DETACH DATABASE: {}", e.to_string()))?;
println!("Datenbank erfolgreich nach '{}' verschlüsselt.", &path);
println!(
"Die Originaldatei '{}' ist unverändert.",
resource_path.as_path().display()
);
/* // Neue Datenbank erstellen
let conn = Connection::open_with_flags(
&path,
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE,
@ -77,7 +132,7 @@ pub fn create_encrypted_database(
"Fehler beim Testen der verschlüsselten Datenbank: {}",
e.to_string()
));
}
} */
// 2. VERSUCHEN, EINE SQLCIPHER-SPEZIFISCHE OPERATION AUSZUFÜHREN
println!("Prüfe SQLCipher-Aktivität mit 'PRAGMA cipher_version;'...");
@ -88,7 +143,7 @@ pub fn create_encrypted_database(
Ok(version) => {
println!("SQLCipher ist aktiv! Version: {}", version);
// Fahre mit normalen Operationen fort
/* // Fahre mit normalen Operationen fort
println!("Erstelle Tabelle 'benutzer'...");
conn.execute(
"CREATE TABLE benutzer (id INTEGER PRIMARY KEY, name TEXT NOT NULL)",
@ -100,7 +155,7 @@ pub fn create_encrypted_database(
.map_err(|e| {
format!("Fehler beim Verschlüsseln der Datenbank: {}", e.to_string())
})?;
println!("Benutzer hinzugefügt.");
println!("Benutzer hinzugefügt."); */
}
Err(e) => {
eprintln!("FEHLER: SQLCipher scheint NICHT aktiv zu sein!");

View File

@ -0,0 +1,181 @@
use mime;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use tauri::{
http::{Request, Response, Uri},
AppHandle, Error as TauriError, Manager, Runtime,
};
pub fn copy_directory(source: String, destination: String) -> Result<(), String> {
println!(
"Kopiere Verzeichnis von '{}' nach '{}'",
source, destination
);
let source_path = PathBuf::from(&source);
let destination_path = PathBuf::from(&destination);
if !source_path.exists() || !source_path.is_dir() {
return Err(format!(
"Quellverzeichnis '{}' nicht gefunden oder ist kein Verzeichnis.",
source
));
}
// Optionen für fs_extra::dir::copy
let mut options = fs_extra::dir::CopyOptions::new();
options.overwrite = true; // Überschreibe Zieldateien, falls sie existieren
options.copy_inside = true; // Kopiere den *Inhalt* des Quellordners in den Zielordner
// options.content_only = true; // Alternative: nur Inhalt kopieren, Zielordner muss existieren
options.buffer_size = 64000; // Standard-Puffergröße, kann angepasst werden
// Führe die Kopieroperation aus
match fs_extra::dir::copy(&source_path, &destination_path, &options) {
Ok(bytes_copied) => {
println!("Verzeichnis erfolgreich kopiert ({} bytes)", bytes_copied);
Ok(()) // Erfolg signalisieren
}
Err(e) => {
eprintln!("Fehler beim Kopieren des Verzeichnisses: {}", e);
Err(format!("Fehler beim Kopieren: {}", e.to_string())) // Fehler als String zurückgeben
}
}
}
pub fn resolve_secure_extension_asset_path<R: Runtime>(
app_handle: &AppHandle<R>,
extension_id: &str,
requested_asset_path: &str,
) -> Result<PathBuf, String> {
// 1. Validiere die Extension ID
if extension_id.is_empty()
|| !extension_id
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Err(format!("Ungültige Extension ID: {}", extension_id));
}
// 2. Bestimme das Basisverzeichnis für alle Erweiterungen (Resource Directory)
let base_extensions_dir = app_handle
.path()
.resource_dir() // Korrekt für Ressourcen
// Wenn du stattdessen App Local Data willst: .app_local_data_dir()
.map_err(|e: TauriError| format!("Basis-Verzeichnis nicht gefunden: {}", e))?
.join("extensions");
// 3. Verzeichnis für die spezifische Erweiterung
let specific_extension_dir = base_extensions_dir.join(extension_id);
// 4. Bereinige den angeforderten Asset-Pfad
let clean_relative_path = requested_asset_path
.replace('\\', "/")
.trim_start_matches('/')
.split('/')
.filter(|&part| !part.is_empty() && part != "." && part != "..")
.collect::<PathBuf>();
if clean_relative_path.as_os_str().is_empty() && requested_asset_path != "/" {
return Err("Leerer oder ungültiger Asset-Pfad".to_string());
}
// 5. Setze den finalen Pfad zusammen
let final_path = specific_extension_dir.join(clean_relative_path);
// 6. SICHERHEITSCHECK (wie vorher)
match final_path.canonicalize() {
Ok(canonical_path) => {
let canonical_base = specific_extension_dir.canonicalize().map_err(|e| {
format!(
"Kann Basis-Pfad '{}' nicht kanonisieren: {}",
specific_extension_dir.display(),
e
)
})?;
if canonical_path.starts_with(&canonical_base) {
Ok(canonical_path)
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err("Ungültiger oder nicht erlaubter Asset-Pfad (kanonisch)".to_string())
}
}
Err(_) => {
// Fehler bei canonicalize (z.B. Pfad existiert nicht)
if final_path.starts_with(&specific_extension_dir) {
Ok(final_path) // Nicht-kanonisierten Pfad zurückgeben
} else {
eprintln!( /* ... Sicherheitswarnung ... */ );
Err("Ungültiger oder nicht erlaubter Asset-Pfad (nicht kanonisiert)".to_string())
}
}
}
}
pub fn handle_extension_protocol<R: Runtime>(
app_handle: &AppHandle<R>,
request: &Request<Vec<u8>>,
) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error>> {
let uri_ref = request.uri(); // uri_ref ist &Uri
println!("Protokoll Handler für: {}", uri_ref);
let uri_string = uri_ref.to_string(); // Konvertiere zu String
let parsed_uri = Uri::from_str(&uri_string)?; // Parse aus &str
let extension_id = parsed_uri
.host()
.ok_or("Kein Host (Extension ID) in URI gefunden")?
.to_string();
let requested_asset_path = parsed_uri.path();
let asset_path_to_load = if requested_asset_path == "/" || requested_asset_path.is_empty() {
"index.html"
} else {
requested_asset_path
};
// Sicheren Dateisystempfad auflösen (nutzt jetzt AppHandle)
let absolute_secure_path =
resolve_secure_extension_asset_path(app_handle, &extension_id, asset_path_to_load)?;
// Datei lesen und Response erstellen (Code wie vorher)
match fs::read(&absolute_secure_path) {
Ok(content) => {
let mime_type = mime_guess::from_path(&absolute_secure_path)
.first_or(mime::APPLICATION_OCTET_STREAM)
.to_string();
println!(
"Liefere {} ({}) für Extension '{}'",
absolute_secure_path.display(),
mime_type,
extension_id
);
// *** KORREKTUR: Verwende Response::builder() ***
Response::builder()
.status(200)
.header("Content-Type", mime_type) // Setze Header über .header()
.body(content) // body() gibt Result<Response<Vec<u8>>, Error> zurück
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
}
Err(e) => {
eprintln!(
"Fehler beim Lesen der Datei {}: {}",
absolute_secure_path.display(),
e
);
let status_code = if e.kind() == std::io::ErrorKind::NotFound {
404
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
403
} else {
500
};
// *** KORREKTUR: Verwende Response::builder() auch für Fehler ***
Response::builder()
.status(status_code)
.body(Vec::new()) // Leerer Body für Fehler
.map_err(|e| e.into()) // Wandle http::Error in Box<dyn Error> um
}
}
}

View File

@ -1,9 +1,11 @@
mod permissions;
use crate::database;
use crate::database::DbConnection;
use crate::models::ExtensionState;
use serde_json::Value as JsonValue;
use std::collections::HashMap;
use tauri::{AppHandle, State};
// Extension-bezogene Funktionen mit extension_-Präfix
/// Lädt eine Extension aus einer Manifest-Datei
/* #[tauri::command]
@ -25,11 +27,11 @@ pub async fn extension_sql_select(
app: AppHandle,
extension_id: String,
sql: String,
params: Vec<String>,
params: Vec<JsonValue>,
state: State<'_, DbConnection>,
) -> Result<Vec<Vec<String>>, String> {
) -> Result<Vec<Vec<JsonValue>>, String> {
permissions::check_read_permission(&app, &extension_id, &sql).await?;
database::core::select(&sql, &params, &state).await
database::core::select(sql, params, &state).await
}
/// Führt SQL-Schreiboperationen mit Berechtigungsprüfung aus
@ -38,9 +40,9 @@ pub async fn extension_sql_execute(
app: AppHandle,
extension_id: String,
sql: String,
params: Vec<String>,
params: Vec<JsonValue>,
state: State<'_, DbConnection>,
) -> Result<String, String> {
) -> Result<usize, String> {
permissions::check_write_permission(&app, &extension_id, &sql).await?;
database::core::execute(&sql, &params, &state).await
database::core::execute(sql, params, &state).await
}

View File

@ -1 +1,8 @@
pub mod core;
pub mod database;
use tauri;
#[tauri::command]
pub async fn copy_directory(source: String, destination: String) -> Result<(), String> {
core::copy_directory(source, destination)
}

View File

@ -9,7 +9,33 @@ use std::sync::Mutex;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let protocol_name = "haex-extension";
tauri::Builder::default()
/* .register_uri_scheme_protocol(protocol_name, move |app_handle, request| {
// Extrahiere den Request aus dem Kontext
//let request = context.request();
// Rufe die Handler-Logik auf
match extension::core::handle_extension_protocol(0, &request) {
Ok(response) => response, // Gib die erfolgreiche Response zurück
Err(e) => {
// Logge den Fehler
eprintln!("Fehler im Protokoll-Handler für '{}': {}", request.uri(), e);
// Gib eine generische 500er Fehler-Response zurück
Response::builder()
.status(500)
.mimetype("text/plain") // Einfacher Text für die Fehlermeldung
.body(format!("Internal Server Error: {}", e).into_bytes()) // Body als Vec<u8>
.unwrap() // .body() kann hier nicht fehlschlagen
}
}
}) */
/* .setup(move |app| {
// Der .setup Hook ist jetzt nur noch für andere Initialisierungen da
// Der AppHandle ist hier nicht mehr nötig für die Protokoll-Registrierung
println!("App Setup abgeschlossen.");
Ok(())
}) */
.plugin(tauri_plugin_http::init())
.manage(DbConnection(Mutex::new(None)))
.manage(ExtensionState::default())
@ -26,7 +52,7 @@ pub fn run() {
database::sql_select,
extension::database::extension_sql_execute,
extension::database::extension_sql_select,
//browser::create_tab
extension::copy_directory //browser::create_tab
])
.run(tauri::generate_context!())
.expect("error while running tauri application");

View File

@ -18,7 +18,11 @@
}
],
"security": {
"csp": null
"csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost",
"assetProtocol": {
"enable": true,
"scope": ["$RESOURCE/extensions/**"]
}
}
},
"bundle": {