16d514f7f1
Virtual Files System: - .cloud Platzhalter-Dateien (JSON mit ID, Name, Groesse, Checksum) - 0 Bytes Speicherverbrauch pro Datei - Doppelklick auf .cloud -> Download + Oeffnen mit Standard-App + Lock - Nach Schliessen: Sync zurueck, lokale Kopie entfernen, .cloud neu - Offline-Markierung: Echte Dateien bleiben lokal (kein .cloud) - Server-Dateien loeschen -> .cloud wird automatisch entfernt Multi-Sync-Pfade (wie Nextcloud): - Beliebig viele Server-Ordner auf lokale Ordner mappen - z.B. /Projekte/2026 -> ~/Projekte oder /Shared/Team -> ~/Team - Freigegebene Ordner von anderen Benutzern sync-bar - Jeder Pfad hat eigenen Modus (Virtual oder Full) - Hinzufuegen/Entfernen/Modus wechseln in der UI Full Sync: - Pro Sync-Pfad waehlbar: Virtual oder Full - Full = alle Dateien lokal spiegeln (bidirektional) - Virtual = .cloud Platzhalter (Standard) - Klick auf Modus-Badge zum Umschalten Ordner-Dialog: - "Durchsuchen..." Button oeffnet nativen Ordner-Auswahl-Dialog - Server-Ordner per Dropdown aus Dateibaum waehlen - Ordner werden automatisch erstellt wenn noetig UI: - Sync-Pfade als Karten: ☁ /Server/Pfad → 📁 /Lokaler/Pfad - Modus-Badge (Virtual/Full) mit Klick zum Wechseln - Tray-Menue: "Jetzt synchronisieren" Eintrag Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
355 lines
11 KiB
Rust
355 lines
11 KiB
Rust
mod sync;
|
|
|
|
use std::path::PathBuf;
|
|
use std::sync::{Arc, Mutex};
|
|
use std::time::Duration;
|
|
use tauri::{
|
|
menu::{Menu, MenuItem},
|
|
tray::TrayIconBuilder,
|
|
AppHandle, Emitter, Manager, State,
|
|
};
|
|
|
|
use sync::api::MiniCloudApi;
|
|
use sync::engine::{SyncEngine, SyncMode, SyncPath};
|
|
use sync::watcher::{FileWatcher, ChangeKind};
|
|
|
|
struct AppState {
|
|
api: Mutex<Option<MiniCloudApi>>,
|
|
sync_engine: Mutex<Option<SyncEngine>>,
|
|
username: Mutex<Option<String>>,
|
|
watchers: Mutex<Vec<FileWatcher>>,
|
|
sync_running: Arc<Mutex<bool>>,
|
|
locked_files: Mutex<Vec<i64>>,
|
|
sync_paths: Mutex<Vec<SyncPath>>,
|
|
}
|
|
|
|
// --- Auth ---
|
|
|
|
#[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,
|
|
}))
|
|
}
|
|
|
|
// --- Sync Paths ---
|
|
|
|
#[tauri::command]
|
|
fn add_sync_path(
|
|
state: State<'_, AppState>,
|
|
server_path: String,
|
|
server_folder_id: Option<i64>,
|
|
local_dir: String,
|
|
mode: String, // "virtual" or "full"
|
|
) -> Result<serde_json::Value, String> {
|
|
let local = PathBuf::from(&local_dir);
|
|
std::fs::create_dir_all(&local).map_err(|e| format!("Ordner erstellen: {}", e))?;
|
|
|
|
let sync_mode = if mode == "full" { SyncMode::Full } else { SyncMode::Virtual };
|
|
let id = format!("{}_{}", server_folder_id.unwrap_or(0), local_dir.replace(['/', '\\'], "_"));
|
|
|
|
let sp = SyncPath {
|
|
id: id.clone(),
|
|
server_path,
|
|
server_folder_id,
|
|
local_dir,
|
|
mode: sync_mode,
|
|
enabled: true,
|
|
};
|
|
|
|
state.sync_paths.lock().unwrap().push(sp.clone());
|
|
|
|
Ok(serde_json::to_value(sp).map_err(|e| e.to_string())?)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn remove_sync_path(state: State<'_, AppState>, id: String) -> Result<String, String> {
|
|
state.sync_paths.lock().unwrap().retain(|p| p.id != id);
|
|
Ok("Sync-Pfad entfernt".to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_sync_paths(state: State<'_, AppState>) -> Result<serde_json::Value, String> {
|
|
let paths = state.sync_paths.lock().unwrap().clone();
|
|
Ok(serde_json::to_value(paths).map_err(|e| e.to_string())?)
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn toggle_sync_mode(state: State<'_, AppState>, id: String) -> Result<String, String> {
|
|
let mut paths = state.sync_paths.lock().unwrap();
|
|
if let Some(p) = paths.iter_mut().find(|p| p.id == id) {
|
|
p.mode = match p.mode {
|
|
SyncMode::Virtual => SyncMode::Full,
|
|
SyncMode::Full => SyncMode::Virtual,
|
|
};
|
|
Ok(format!("Modus: {:?}", p.mode))
|
|
} else {
|
|
Err("Pfad nicht gefunden".to_string())
|
|
}
|
|
}
|
|
|
|
// --- Sync ---
|
|
|
|
#[tauri::command]
|
|
async fn start_sync(app: AppHandle, state: State<'_, AppState>) -> Result<Vec<String>, String> {
|
|
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
|
|
let paths = state.sync_paths.lock().unwrap().clone();
|
|
|
|
if paths.is_empty() {
|
|
return Err("Keine Sync-Pfade konfiguriert".to_string());
|
|
}
|
|
|
|
let mut engine = SyncEngine::new(api.clone());
|
|
engine.sync_paths = paths.clone();
|
|
let log = engine.sync_all().await?;
|
|
|
|
*state.sync_engine.lock().unwrap() = Some(engine);
|
|
|
|
// Start watchers for each sync path
|
|
let mut watchers = Vec::new();
|
|
for sp in &paths {
|
|
if let Ok(w) = FileWatcher::new(&PathBuf::from(&sp.local_dir)) {
|
|
watchers.push(w);
|
|
}
|
|
}
|
|
*state.watchers.lock().unwrap() = watchers;
|
|
|
|
// Start background threads
|
|
start_background_sync(app, state.sync_running.clone(), api, paths);
|
|
|
|
Ok(log)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn run_sync_now(state: State<'_, AppState>) -> Result<Vec<String>, String> {
|
|
let mut engine = {
|
|
let mut guard = state.sync_engine.lock().unwrap();
|
|
guard.take().ok_or("Sync nicht gestartet")?
|
|
};
|
|
let result = engine.sync_all().await;
|
|
*state.sync_engine.lock().unwrap() = Some(engine);
|
|
result
|
|
}
|
|
|
|
// --- File Operations ---
|
|
|
|
#[tauri::command]
|
|
async fn open_cloud_file(state: State<'_, AppState>, cloud_path: String) -> Result<String, String> {
|
|
let engine = {
|
|
let guard = state.sync_engine.lock().unwrap();
|
|
guard.as_ref().ok_or("Sync nicht gestartet")?.api.clone()
|
|
};
|
|
|
|
let path = PathBuf::from(&cloud_path);
|
|
let content = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
|
|
let placeholder: serde_json::Value = serde_json::from_str(&content).map_err(|e| e.to_string())?;
|
|
let file_id = placeholder.get("id").and_then(|v| v.as_i64()).ok_or("Keine ID")?;
|
|
let file_name = placeholder.get("name").and_then(|v| v.as_str()).unwrap_or("file");
|
|
|
|
let real_path = path.parent().unwrap().join(file_name);
|
|
|
|
// Download
|
|
engine.download_file(file_id, &real_path).await?;
|
|
|
|
// Remove placeholder
|
|
std::fs::remove_file(&path).ok();
|
|
|
|
// Lock
|
|
let _ = engine.lock_file(file_id, "Desktop Sync Client").await;
|
|
state.locked_files.lock().unwrap().push(file_id);
|
|
|
|
// Open with default application
|
|
let _ = open::that(&real_path);
|
|
|
|
Ok(real_path.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())?)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_status(state: State<'_, AppState>) -> Result<serde_json::Value, String> {
|
|
let logged_in = state.api.lock().unwrap().is_some();
|
|
let username = state.username.lock().unwrap().clone();
|
|
let syncing = *state.sync_running.lock().unwrap();
|
|
let paths = state.sync_paths.lock().unwrap().len();
|
|
let locked = state.locked_files.lock().unwrap().len();
|
|
|
|
Ok(serde_json::json!({
|
|
"logged_in": logged_in,
|
|
"username": username,
|
|
"syncing": syncing,
|
|
"sync_paths": paths,
|
|
"locked_files": locked,
|
|
}))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn lock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result<String, String> {
|
|
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
|
|
api.lock_file(file_id, "Desktop Sync Client").await?;
|
|
state.locked_files.lock().unwrap().push(file_id);
|
|
Ok("Datei gesperrt".to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn unlock_file_cmd(state: State<'_, AppState>, file_id: i64) -> Result<String, String> {
|
|
let api = state.api.lock().unwrap().clone().ok_or("Nicht eingeloggt")?;
|
|
api.unlock_file(file_id).await?;
|
|
state.locked_files.lock().unwrap().retain(|&id| id != file_id);
|
|
Ok("Datei entsperrt".to_string())
|
|
}
|
|
|
|
// --- Background Threads ---
|
|
|
|
fn start_background_sync(
|
|
app: AppHandle,
|
|
sync_running: Arc<Mutex<bool>>,
|
|
api: MiniCloudApi,
|
|
paths: Vec<SyncPath>,
|
|
) {
|
|
// Auto-sync every 30 seconds
|
|
let app_sync = app.clone();
|
|
let api_sync = api.clone();
|
|
let paths_sync = paths.clone();
|
|
|
|
std::thread::spawn(move || {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
let mut engine = SyncEngine::new(api_sync);
|
|
engine.sync_paths = paths_sync;
|
|
|
|
loop {
|
|
std::thread::sleep(Duration::from_secs(30));
|
|
|
|
*sync_running.lock().unwrap() = true;
|
|
let _ = app_sync.emit("sync-status", "syncing");
|
|
|
|
match rt.block_on(engine.sync_all()) {
|
|
Ok(log) => {
|
|
if !log.is_empty() {
|
|
let _ = app_sync.emit("sync-log", log);
|
|
}
|
|
}
|
|
Err(e) => { let _ = app_sync.emit("sync-error", e); }
|
|
}
|
|
|
|
*sync_running.lock().unwrap() = false;
|
|
let _ = app_sync.emit("sync-status", "synced");
|
|
}
|
|
});
|
|
|
|
// Heartbeat every 60 seconds
|
|
let app_hb = app.clone();
|
|
let api_hb = api.clone();
|
|
std::thread::spawn(move || {
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
loop {
|
|
std::thread::sleep(Duration::from_secs(60));
|
|
let state = app_hb.state::<AppState>();
|
|
let locked = state.locked_files.lock().unwrap().clone();
|
|
for file_id in locked {
|
|
let _ = rt.block_on(api_hb.heartbeat(file_id));
|
|
}
|
|
}
|
|
});
|
|
|
|
// File watcher processing
|
|
let app_w = app.clone();
|
|
std::thread::spawn(move || {
|
|
loop {
|
|
std::thread::sleep(Duration::from_secs(2));
|
|
let state = app_w.state::<AppState>();
|
|
let watchers = state.watchers.lock().unwrap();
|
|
for watcher in watchers.iter() {
|
|
while let Ok(change) = watcher.receiver.try_recv() {
|
|
let name = change.path.file_name()
|
|
.and_then(|n| n.to_str()).unwrap_or("?");
|
|
let msg = match change.kind {
|
|
ChangeKind::Created => format!("Neu: {}", name),
|
|
ChangeKind::Modified => format!("Geaendert: {}", name),
|
|
ChangeKind::Deleted => format!("Geloescht: {}", name),
|
|
};
|
|
let _ = app_w.emit("file-change", msg);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// --- App Setup ---
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
tauri::Builder::default()
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_notification::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
.manage(AppState {
|
|
api: Mutex::new(None),
|
|
sync_engine: Mutex::new(None),
|
|
username: Mutex::new(None),
|
|
watchers: Mutex::new(Vec::new()),
|
|
sync_running: Arc::new(Mutex::new(false)),
|
|
locked_files: Mutex::new(Vec::new()),
|
|
sync_paths: Mutex::new(Vec::new()),
|
|
})
|
|
.setup(|app| {
|
|
let quit = MenuItem::with_id(app, "quit", "Beenden", true, None::<&str>)?;
|
|
let show = MenuItem::with_id(app, "show", "Oeffnen", true, None::<&str>)?;
|
|
let sync_now = MenuItem::with_id(app, "sync", "Jetzt synchronisieren", true, None::<&str>)?;
|
|
let menu = Menu::with_items(app, &[&show, &sync_now, &quit])?;
|
|
|
|
TrayIconBuilder::new()
|
|
.tooltip("Mini-Cloud Sync")
|
|
.menu(&menu)
|
|
.on_menu_event(|app, event| {
|
|
match event.id.as_ref() {
|
|
"quit" => std::process::exit(0),
|
|
"show" => {
|
|
if let Some(w) = app.get_webview_window("main") {
|
|
let _ = w.show();
|
|
let _ = w.set_focus();
|
|
}
|
|
}
|
|
"sync" => { let _ = app.emit("trigger-sync", ()); }
|
|
_ => {}
|
|
}
|
|
})
|
|
.build(app)?;
|
|
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
login,
|
|
add_sync_path,
|
|
remove_sync_path,
|
|
get_sync_paths,
|
|
toggle_sync_mode,
|
|
start_sync,
|
|
run_sync_now,
|
|
open_cloud_file,
|
|
get_file_tree,
|
|
get_status,
|
|
lock_file_cmd,
|
|
unlock_file_cmd,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running tauri application");
|
|
}
|