feat(cloud-files): Geteilte Ordner + Rechtsklick-Menue

Backend:
- /api/sync/tree liefert jetzt {tree, shared} - shared enthaelt alle
  Dateien die MIT dem Benutzer geteilt wurden (FilePermission), nur
  Top-Level-Shares, mit Owner-Name im Anzeigenamen
- updated_at zusaetzlich als modified_at im Response fuer Client-
  Kompatibilitaet

Client:
- fetch_remote_entries merged Shared-Subtree unter virtuellem Ordner
  "Geteilt mit mir" (synthetische ID -1) in den Mount-Point
- modified_at faellt auf updated_at zurueck, falls nicht vorhanden

Kontextmenue:
- Neue HKCU-Registry-Eintraege fuer "Immer offline verfuegbar" und
  "Speicher freigeben", AppliesTo filtert auf Mount-Pfad, sodass die
  Verben nur bei Dateien unterhalb des Sync-Ordners erscheinen
- Aufruf der eigenen .exe mit --pin / --unpin <file>
- handle_cli_shortcuts fuehrt die Aktion aus und beendet sofort,
  ohne die UI/Tray/Single-Instance-Logik anzustossen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker
2026-04-23 11:15:04 +02:00
parent 4026defe79
commit 78cfbf1ad3
3 changed files with 169 additions and 17 deletions
@@ -102,12 +102,63 @@ pub fn install(
ns.set_value("", &display_name.to_string())
.map_err(|e| format!("set ns name: {e}"))?;
// 7) Explorer informieren (SHChangeNotify)
// 7) Kontext-Menue-Verben (Rechtsklick) fuer Dateien unter dem Mount
install_context_menu(mount_point)?;
// 8) Explorer informieren (SHChangeNotify)
notify_shell();
Ok(())
}
/// Registriert "Immer offline halten" / "Speicher freigeben" als
/// Rechtsklick-Menuepunkte, die nur fuer Dateien unterhalb des Mounts
/// angezeigt werden (AppliesTo-Filter).
fn install_context_menu(mount_point: &Path) -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let exe = std::env::current_exe()
.map_err(|e| format!("current_exe: {e}"))?
.to_string_lossy()
.into_owned();
let mount_str = mount_point.to_string_lossy();
// AppliesTo filtert die Verben auf Pfade, die unter dem Mount liegen.
// Syntax: "System.ItemPathDisplay:~< \"C:\\Users\\..\\Mini-Cloud\\\""
let applies_to = format!(
"System.ItemPathDisplay:~< \"{}\\\"",
mount_str.replace('\\', "\\\\")
);
for (verb, label, flag) in [
("MiniCloudPin", "Immer offline verfuegbar", "--pin"),
("MiniCloudUnpin", "Speicher freigeben", "--unpin"),
] {
let key_path = format!("Software\\Classes\\*\\shell\\{}", verb);
let (k, _) = hkcu
.create_subkey(&key_path)
.map_err(|e| format!("verb {verb}: {e}"))?;
k.set_value("MUIVerb", &label.to_string())
.map_err(|e| format!("MUIVerb: {e}"))?;
k.set_value("AppliesTo", &applies_to)
.map_err(|e| format!("AppliesTo: {e}"))?;
k.set_value("Icon", &exe)
.map_err(|e| format!("Icon: {e}"))?;
let (cmd, _) = k
.create_subkey("command")
.map_err(|e| format!("cmd: {e}"))?;
cmd.set_value("", &format!("\"{}\" {} \"%1\"", exe, flag))
.map_err(|e| format!("cmdline: {e}"))?;
}
Ok(())
}
fn uninstall_context_menu() {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
for verb in ["MiniCloudPin", "MiniCloudUnpin"] {
let _ = hkcu.delete_subkey_all(format!("Software\\Classes\\*\\shell\\{}", verb));
}
}
/// Entferne die Shell-Integration wieder.
pub fn uninstall() -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
@@ -121,6 +172,8 @@ pub fn uninstall() -> Result<(), String> {
let clsid_path = format!("Software\\Classes\\CLSID\\{}", CLSID_GUID);
let _ = hkcu.delete_subkey_all(&clsid_path);
uninstall_context_menu();
notify_shell();
Ok(())
}
+52
View File
@@ -1030,8 +1030,15 @@ async fn fetch_remote_entries(
.as_array()
.cloned()
.unwrap_or_default();
let shared = json
.get("shared")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
// Rekursiv flach machen (Struktur parent_id beibehalten).
// modified_at akzeptiert beides: das neue "modified_at" oder das
// alte "updated_at" als Fallback.
fn walk(
nodes: &[serde_json::Value],
parent: Option<i64>,
@@ -1049,6 +1056,7 @@ async fn fetch_remote_entries(
let modified_at = n
.get("modified_at")
.and_then(|x| x.as_str())
.or_else(|| n.get("updated_at").and_then(|x| x.as_str()))
.unwrap_or("")
.to_string();
let checksum = n
@@ -1071,11 +1079,55 @@ async fn fetch_remote_entries(
}
let mut flat = Vec::new();
walk(&tree, None, &mut flat);
// Virtueller Ordner "Geteilt mit mir" nur dann, wenn es geteilte
// Dateien gibt. ID -1 ist reserviert dafuer (keine Kollision
// mit echten DB-IDs).
if !shared.is_empty() {
flat.push(cloud_files::RemoteEntry {
id: -1,
name: "Geteilt mit mir".to_string(),
parent_id: None,
is_folder: true,
size: 0,
modified_at: String::new(),
checksum: None,
});
walk(&shared, Some(-1), &mut flat);
}
Ok(flat)
}
/// Short-circuit fuer Shell-Kontextmenue-Aufrufe:
/// `minicloud-sync --pin <file>` oder `--unpin <file>` fuehrt die
/// Aktion direkt aus und beendet. Kein UI, kein Tray.
#[cfg(windows)]
fn handle_cli_shortcuts() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 3 {
return;
}
let cmd = args[1].as_str();
let path = std::path::PathBuf::from(&args[2]);
let result = match cmd {
"--pin" => cloud_files::pin_file(&path),
"--unpin" => cloud_files::unpin_file(&path),
_ => return,
};
if let Err(e) = result {
eprintln!("[cloud_files CLI] {cmd} failed: {e}");
std::process::exit(1);
}
std::process::exit(0);
}
#[cfg(not(windows))]
fn handle_cli_shortcuts() {}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
handle_cli_shortcuts();
handle_single_instance();
tauri::Builder::default()