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:
@@ -42,8 +42,10 @@ windows = { version = "0.58", features = [
|
|||||||
"Win32_System_CorrelationVector", # gate fuer CF_CALLBACK_INFO / CfExecute / CfConnectSyncRoot
|
"Win32_System_CorrelationVector", # gate fuer CF_CALLBACK_INFO / CfExecute / CfConnectSyncRoot
|
||||||
"Win32_UI_Shell",
|
"Win32_UI_Shell",
|
||||||
"Win32_Security",
|
"Win32_Security",
|
||||||
|
"Win32_System_Registry",
|
||||||
] }
|
] }
|
||||||
widestring = "1"
|
widestring = "1"
|
||||||
|
winreg = "0.52"
|
||||||
|
|
||||||
# Linux: FUSE-basiertes Virtual-Filesystem (optional, cargo build --features linux_fuse)
|
# Linux: FUSE-basiertes Virtual-Filesystem (optional, cargo build --features linux_fuse)
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ pub enum SyncState {
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub mod windows;
|
pub mod windows;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub mod shell_integration;
|
||||||
#[cfg(all(target_os = "linux", feature = "linux_fuse"))]
|
#[cfg(all(target_os = "linux", feature = "linux_fuse"))]
|
||||||
pub mod linux;
|
pub mod linux;
|
||||||
pub mod sync_loop;
|
pub mod sync_loop;
|
||||||
|
|||||||
@@ -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())
|
||||||
|
}
|
||||||
@@ -136,11 +136,22 @@ pub fn register_sync_root(
|
|||||||
log_msg(mount_point, "CfRegisterSyncRoot OK");
|
log_msg(mount_point, "CfRegisterSyncRoot OK");
|
||||||
connect_callbacks(mount_point)?;
|
connect_callbacks(mount_point)?;
|
||||||
log_msg(mount_point, "callbacks connected");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn unregister_sync_root(mount_point: &PathBuf) -> Result<(), String> {
|
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())
|
let path_wide = U16CString::from_str(mount_point.to_string_lossy().as_ref())
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
unsafe {
|
unsafe {
|
||||||
|
|||||||
Reference in New Issue
Block a user