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
@@ -0,0 +1,58 @@
use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::path::PathBuf;
use std::sync::mpsc;
pub struct FileWatcher {
_watcher: RecommendedWatcher,
pub receiver: mpsc::Receiver<FileChange>,
}
#[derive(Debug, Clone)]
pub struct FileChange {
pub path: PathBuf,
pub kind: ChangeKind,
}
#[derive(Debug, Clone)]
pub enum ChangeKind {
Created,
Modified,
Deleted,
}
impl FileWatcher {
pub fn new(watch_dir: &PathBuf) -> Result<Self, String> {
let (tx, rx) = mpsc::channel();
let mut watcher = RecommendedWatcher::new(
move |result: Result<Event, notify::Error>| {
if let Ok(event) = result {
let kind = match event.kind {
EventKind::Create(_) => Some(ChangeKind::Created),
EventKind::Modify(_) => Some(ChangeKind::Modified),
EventKind::Remove(_) => Some(ChangeKind::Deleted),
_ => None,
};
if let Some(kind) = kind {
for path in event.paths {
// Skip hidden files and temp files
let name = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
if name.starts_with('.') || name.starts_with('~') || name.ends_with(".tmp") {
continue;
}
let _ = tx.send(FileChange { path, kind: kind.clone() });
}
}
}
},
Config::default(),
).map_err(|e| format!("Watcher-Fehler: {}", e))?;
watcher.watch(watch_dir.as_ref(), RecursiveMode::Recursive)
.map_err(|e| format!("Watch-Fehler: {}", e))?;
Ok(Self { _watcher: watcher, receiver: rx })
}
}