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:
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user