feat(cloud-files): Explorer-Sidebar-Integration fuer Windows

Registriert den Sync-Ordner als Shell-Namespace-Extension unter
HKEY_CURRENT_USER (kein Admin noetig), sodass er mit eigenem Icon
in der linken Leiste des Datei-Explorers erscheint - wie bei
OneDrive oder Dropbox.

- Neues Modul cloud_files::shell_integration mit install/uninstall
- Registry-Eintraege unter HKCU\Software\Classes\CLSID\{GUID} und
  HKCU\...\Explorer\Desktop\NameSpace\{GUID}
- Nutzt die laufende .exe als Icon-Quelle (fallback: imageres.dll)
- SHChangeNotify(SHCNE_ASSOCCHANGED) damit Explorer sofort aktualisiert
- install/uninstall werden aus register_sync_root/unregister aufgerufen
- winreg-Crate fuer sauberen Registry-Zugriff

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-22 15:47:05 +02:00
parent 2937082ba2
commit 4026defe79
4 changed files with 159 additions and 1 deletions

View File

@ -42,8 +42,10 @@ windows = { version = "0.58", features = [
"Win32_System_CorrelationVector", # gate fuer CF_CALLBACK_INFO / CfExecute / CfConnectSyncRoot
"Win32_UI_Shell",
"Win32_Security",
"Win32_System_Registry",
] }
widestring = "1"
winreg = "0.52"
# Linux: FUSE-basiertes Virtual-Filesystem (optional, cargo build --features linux_fuse)
[target.'cfg(target_os = "linux")'.dependencies]

View File

@ -43,6 +43,8 @@ pub enum SyncState {
#[cfg(windows)]
pub mod windows;
#[cfg(windows)]
pub mod shell_integration;
#[cfg(all(target_os = "linux", feature = "linux_fuse"))]
pub mod linux;
pub mod sync_loop;

View File

@ -0,0 +1,143 @@
//! Explorer-Sidebar-Integration fuer Windows (ohne Admin-Rechte).
//!
//! Registriert den Sync-Ordner als Shell-Namespace-Extension unter
//! HKEY_CURRENT_USER, sodass er mit eigenem Icon in der Navigation
//! des Datei-Explorers erscheint (wie OneDrive/Dropbox).
//!
//! Anders als die eigentliche Cloud Files API ist das reine Registry-
//! Kosmetik - der Ordner funktioniert auch ohne Sidebar-Eintrag,
//! nur sieht man ihn dann nicht in der linken Leiste.
#![cfg(windows)]
use std::path::Path;
use winreg::enums::*;
use winreg::RegKey;
// Stabile GUID fuer Mini-Cloud - gleiche wie in windows.rs als ProviderId.
const CLSID_GUID: &str = "{4D696E69-436C-6F75-6444-7566667944AB}";
// Standard-CLSID fuer "Generic Shell Folder Implementation".
const SHELL_FOLDER_CLSID: &str = "{0E5AAE11-A475-4c5b-AB00-C66DE400274E}";
/// Registriere den Mount-Ordner in der Explorer-Navigation.
/// `icon_source`: Pfad zu ICO oder EXE mit Icon-Index (z.B. "C:\\app.exe,0")
pub fn install(
display_name: &str,
mount_point: &Path,
icon_source: &str,
) -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
// 1) CLSID-Eintrag unter Software\Classes\CLSID\{GUID}
let clsid_path = format!("Software\\Classes\\CLSID\\{}", CLSID_GUID);
let (clsid, _) = hkcu
.create_subkey(&clsid_path)
.map_err(|e| format!("create CLSID: {e}"))?;
clsid
.set_value("", &display_name.to_string())
.map_err(|e| format!("set displayname: {e}"))?;
clsid
.set_value("System.IsPinnedToNameSpaceTree", &1u32)
.map_err(|e| format!("set pinned: {e}"))?;
clsid
.set_value("SortOrderIndex", &0x42u32)
.map_err(|e| format!("set sortorder: {e}"))?;
// 2) DefaultIcon
let (icon_key, _) = clsid
.create_subkey("DefaultIcon")
.map_err(|e| format!("create DefaultIcon: {e}"))?;
icon_key
.set_value("", &icon_source.to_string())
.map_err(|e| format!("set icon: {e}"))?;
// 3) InProcServer32 -> shell32.dll (Standard Shell-Folder-Host)
let (inproc, _) = clsid
.create_subkey("InProcServer32")
.map_err(|e| format!("create InProcServer32: {e}"))?;
inproc
.set_value("", &"%SystemRoot%\\system32\\shell32.dll".to_string())
.map_err(|e| format!("set shell32: {e}"))?;
inproc
.set_value("ThreadingModel", &"Both".to_string())
.map_err(|e| format!("set threading: {e}"))?;
// 4) Instance -> zeigt auf generischen Shell-Folder
let (instance, _) = clsid
.create_subkey("Instance")
.map_err(|e| format!("create Instance: {e}"))?;
instance
.set_value("CLSID", &SHELL_FOLDER_CLSID.to_string())
.map_err(|e| format!("set inst clsid: {e}"))?;
let (pb, _) = instance
.create_subkey("InitPropertyBag")
.map_err(|e| format!("create InitPropertyBag: {e}"))?;
pb.set_value("Attributes", &0x11u32)
.map_err(|e| format!("set attrs pb: {e}"))?;
pb.set_value(
"TargetFolderPath",
&mount_point.to_string_lossy().into_owned(),
)
.map_err(|e| format!("set target: {e}"))?;
// 5) ShellFolder-Flags
let (sf, _) = clsid
.create_subkey("ShellFolder")
.map_err(|e| format!("create ShellFolder: {e}"))?;
sf.set_value("FolderValueFlags", &0x28u32)
.map_err(|e| format!("set folderflags: {e}"))?;
sf.set_value("Attributes", &0xF080004Du32)
.map_err(|e| format!("set attrs sf: {e}"))?;
// 6) In die Navigation einhaengen
let ns_path = format!(
"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{}",
CLSID_GUID
);
let (ns, _) = hkcu
.create_subkey(&ns_path)
.map_err(|e| format!("create NameSpace: {e}"))?;
ns.set_value("", &display_name.to_string())
.map_err(|e| format!("set ns name: {e}"))?;
// 7) Explorer informieren (SHChangeNotify)
notify_shell();
Ok(())
}
/// Entferne die Shell-Integration wieder.
pub fn uninstall() -> Result<(), String> {
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
let ns_path = format!(
"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Desktop\\NameSpace\\{}",
CLSID_GUID
);
let _ = hkcu.delete_subkey_all(&ns_path);
let clsid_path = format!("Software\\Classes\\CLSID\\{}", CLSID_GUID);
let _ = hkcu.delete_subkey_all(&clsid_path);
notify_shell();
Ok(())
}
/// Teilt Explorer mit, dass sich die Shell-Namespace-Liste geaendert hat.
/// Ohne das sieht man den neuen Eintrag erst nach Explorer-Neustart.
fn notify_shell() {
use windows::Win32::UI::Shell::{SHChangeNotify, SHCNE_ASSOCCHANGED, SHCNF_IDLIST};
unsafe {
SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None);
}
}
/// Standard-Icon-Quelle: die laufende .exe mit Index 0.
pub fn default_icon_source() -> String {
std::env::current_exe()
.ok()
.and_then(|p| p.to_str().map(|s| format!("{},0", s)))
.unwrap_or_else(|| "%SystemRoot%\\system32\\imageres.dll,2".to_string())
}

View File

@ -136,11 +136,22 @@ pub fn register_sync_root(
log_msg(mount_point, "CfRegisterSyncRoot OK");
connect_callbacks(mount_point)?;
log_msg(mount_point, "callbacks connected");
// Explorer-Sidebar-Eintrag mit Wolken-Icon
let icon = super::shell_integration::default_icon_source();
match super::shell_integration::install(provider_name, mount_point, &icon) {
Ok(()) => log_msg(mount_point, "shell integration installed"),
Err(e) => log_err(mount_point, &format!("shell integration FAILED: {e}")),
}
Ok(())
}
pub fn unregister_sync_root(mount_point: &PathBuf) -> Result<(), String> {
disconnect_callbacks()?;
// Shell-Eintrag zuerst entfernen (schlaegt nie fehl).
let _ = super::shell_integration::uninstall();
let _ = disconnect_callbacks();
let path_wide = U16CString::from_str(mount_point.to_string_lossy().as_ref())
.map_err(|e| e.to_string())?;
unsafe {