feat: Desktop Sync Client (Tauri) - Grundgeruest

Tauri 2 Desktop-Client mit:

Rust-Backend:
- MiniCloudApi: Login, Token-Refresh, Upload, Download, Sync-Tree,
  Sync-Changes, File-Locking (Lock/Unlock/Heartbeat)
- SyncEngine: Full-Sync (Server-Tree vs. lokales Dateisystem),
  Delta-Sync (nur Aenderungen seit letztem Sync), bidirektionaler
  Abgleich mit SHA-256 Checksummen, Ordner-Erstellung,
  Lock-Status-Pruefung vor Upload, Konflikt-Erkennung
- FileWatcher: Filesystem-Watcher (notify crate) fuer Echtzeit-
  Erkennung lokaler Aenderungen, filtert temp/hidden files

Vue-Frontend:
- Login-Screen: Server-URL, Benutzername, Passwort
- Main-Screen: Sync-Ordner setzen, Sync starten, Dateiliste mit
  Lock-Status, Sync-Protokoll
- Dark-Mode Support

Tauri-Kommandos: login, set_sync_dir, start_sync, delta_sync,
  get_status, get_file_tree

Zum Bauen (Linux):
  sudo apt install libwebkit2gtk-4.1-dev libgtk-3-dev
  cd clients/desktop && npm install && npm run tauri build

Windows/Mac: Tauri Voraussetzungen installieren, dann gleicher Befehl

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-11 23:26:57 +02:00
parent 748537b9f5
commit 06ad65dbb3
39 changed files with 8735 additions and 0 deletions
+108
View File
@@ -0,0 +1,108 @@
mod sync;
use std::path::PathBuf;
use std::sync::Mutex;
use tauri::State;
use sync::api::MiniCloudApi;
use sync::engine::SyncEngine;
struct AppState {
api: Mutex<Option<MiniCloudApi>>,
sync_engine: Mutex<Option<SyncEngine>>,
sync_dir: Mutex<Option<PathBuf>>,
username: Mutex<Option<String>>,
}
#[tauri::command]
async fn login(
state: State<'_, AppState>,
server_url: String,
username: String,
password: String,
) -> Result<serde_json::Value, String> {
let mut api = MiniCloudApi::new(&server_url);
let result = api.login(&username, &password).await?;
*state.api.lock().unwrap() = Some(api);
*state.username.lock().unwrap() = Some(username);
Ok(serde_json::json!({
"username": result.user.username,
"role": result.user.role,
}))
}
#[tauri::command]
fn set_sync_dir(state: State<'_, AppState>, path: String) -> Result<String, String> {
let sync_path = PathBuf::from(&path);
if !sync_path.exists() {
std::fs::create_dir_all(&sync_path).map_err(|e| e.to_string())?;
}
*state.sync_dir.lock().unwrap() = Some(sync_path);
Ok(format!("Sync-Ordner gesetzt: {}", path))
}
#[tauri::command]
async fn start_sync(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let api = state.api.lock().unwrap().clone()
.ok_or("Nicht eingeloggt")?;
let sync_dir = state.sync_dir.lock().unwrap().clone()
.ok_or("Kein Sync-Ordner gesetzt")?;
let mut engine = SyncEngine::new(sync_dir, api);
let log = engine.full_sync().await?;
*state.sync_engine.lock().unwrap() = Some(engine);
Ok(log)
}
#[tauri::command]
async fn delta_sync(state: State<'_, AppState>) -> Result<Vec<String>, String> {
let mut engine_guard = state.sync_engine.lock().unwrap();
let engine = engine_guard.as_mut().ok_or("Sync nicht gestartet")?;
engine.delta_sync().await
}
#[tauri::command]
async fn get_status(state: State<'_, AppState>) -> Result<serde_json::Value, String> {
let logged_in = state.api.lock().unwrap().is_some();
let sync_dir = state.sync_dir.lock().unwrap().clone();
let username = state.username.lock().unwrap().clone();
Ok(serde_json::json!({
"logged_in": logged_in,
"username": username,
"sync_dir": sync_dir.map(|p| p.to_string_lossy().to_string()),
}))
}
#[tauri::command]
async fn get_file_tree(state: State<'_, AppState>) -> Result<serde_json::Value, String> {
let api = state.api.lock().unwrap().clone()
.ok_or("Nicht eingeloggt")?;
let tree = api.get_sync_tree().await?;
Ok(serde_json::to_value(tree).map_err(|e| e.to_string())?)
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_opener::init())
.manage(AppState {
api: Mutex::new(None),
sync_engine: Mutex::new(None),
sync_dir: Mutex::new(None),
username: Mutex::new(None),
})
.invoke_handler(tauri::generate_handler![
login,
set_sync_dir,
start_sync,
delta_sync,
get_status,
get_file_tree,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}