feat(windows): implement explorer shell integration, auto drive allocation, auto-open, and system tray icon
This commit is contained in:
@@ -5,3 +5,4 @@ pub mod storage;
|
||||
pub mod ui;
|
||||
pub mod verify;
|
||||
pub mod vfs;
|
||||
pub mod windows;
|
||||
|
||||
+53
-4
@@ -41,9 +41,9 @@ enum Commands {
|
||||
#[arg(short, long)]
|
||||
path: PathBuf,
|
||||
|
||||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:')
|
||||
/// Laufwerksbuchstabe (z. B. 'S' oder 'S:', optional; wählt standardmäßig automatisch das nächste freie Laufwerk)
|
||||
#[arg(short, long)]
|
||||
drive: String,
|
||||
drive: Option<String>,
|
||||
|
||||
/// Optionaler TCP-Port für den lokalen WebDAV-Server (Standard: 8443)
|
||||
#[arg(long)]
|
||||
@@ -52,6 +52,14 @@ enum Commands {
|
||||
/// Optionaler 24-Wort Notfall-Wiederherstellungsschlüssel (umgeht Passwortabfrage)
|
||||
#[arg(long)]
|
||||
recovery_key: Option<String>,
|
||||
|
||||
/// Öffnet das Netzlaufwerk nach dem Mounten nicht automatisch im Windows Explorer
|
||||
#[arg(long, default_value_t = false)]
|
||||
no_open: bool,
|
||||
|
||||
/// Deaktiviert das Windows System-Tray Icon während des Mounts
|
||||
#[arg(long, default_value_t = false)]
|
||||
no_tray: bool,
|
||||
},
|
||||
|
||||
/// Trennt ein eingebundenes Netzlaufwerk manuell
|
||||
@@ -115,6 +123,12 @@ enum Commands {
|
||||
#[arg(long, default_value_t = true)]
|
||||
full: bool,
|
||||
},
|
||||
|
||||
/// Registriert .sanctum Containerdateien im Windows Explorer (Doppelklick & Kontextmenü, keine Adminrechte)
|
||||
Register,
|
||||
|
||||
/// Entfernt die .sanctum Explorer-Integration aus der Benutzer-Registry
|
||||
Unregister,
|
||||
}
|
||||
|
||||
fn parse_drive_letter(s: &str) -> Result<char> {
|
||||
@@ -465,8 +479,21 @@ async fn run() -> Result<()> {
|
||||
drive,
|
||||
port,
|
||||
recovery_key,
|
||||
no_open,
|
||||
no_tray,
|
||||
} => {
|
||||
let drive_char = parse_drive_letter(&drive)?;
|
||||
let drive_char = match drive {
|
||||
Some(ref d) => parse_drive_letter(d)?,
|
||||
None => {
|
||||
let auto_drive = sanctum::windows::find_next_available_drive()?;
|
||||
println!(
|
||||
" {} Kein Laufwerksbuchstabe angegeben. Wähle automatisch freien Buchstaben {}:",
|
||||
ui::cyan("ℹ"),
|
||||
auto_drive
|
||||
);
|
||||
auto_drive
|
||||
}
|
||||
};
|
||||
let auth = if let Some(key) = recovery_key {
|
||||
ContainerAuth::RecoveryKey(key)
|
||||
} else {
|
||||
@@ -479,7 +506,9 @@ async fn run() -> Result<()> {
|
||||
ContainerAuth::Password(password)
|
||||
};
|
||||
|
||||
mount_container(&path, drive_char, port, auth).await?;
|
||||
let open_explorer = !no_open;
|
||||
let enable_tray = !no_tray;
|
||||
mount_container(&path, drive_char, port, auth, open_explorer, enable_tray).await?;
|
||||
}
|
||||
Commands::Unmount { drive } => {
|
||||
let drive_char = parse_drive_letter(&drive)?;
|
||||
@@ -509,6 +538,26 @@ async fn run() -> Result<()> {
|
||||
Commands::Verify { path, full } => {
|
||||
handle_verify(&path, full)?;
|
||||
}
|
||||
Commands::Register => {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Windows Explorer Integration einrichten │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
sanctum::windows::register_explorer_integration()?;
|
||||
println!(" {} .sanctum-Dateien wurden erfolgreich im Windows Explorer verknüpft!", ui::green("✔"));
|
||||
println!(" • Doppelklick: Öffnet und bindet den Container direkt als Laufwerk ein");
|
||||
println!(" • Rechtsklick: Bietet Optionen für Integritätsprüfung (FSCK) und Header-Backup");
|
||||
println!();
|
||||
}
|
||||
Commands::Unregister => {
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ Sanctum — Windows Explorer Integration entfernen │");
|
||||
println!("└─────────────────────────────────────────────────────────────┘");
|
||||
println!();
|
||||
sanctum::windows::unregister_explorer_integration()?;
|
||||
println!(" {} .sanctum Dateiverknüpfungen wurden aus der Registry entfernt.", ui::green("✔"));
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
+57
-8
@@ -81,6 +81,8 @@ pub async fn mount_container(
|
||||
drive_letter: char,
|
||||
requested_port: Option<u16>,
|
||||
auth: ContainerAuth,
|
||||
open_explorer: bool,
|
||||
enable_tray: bool,
|
||||
) -> Result<()> {
|
||||
let drive_str = format_drive(drive_letter);
|
||||
|
||||
@@ -207,6 +209,45 @@ pub async fn mount_container(
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// Optional automatisch im Windows Explorer öffnen
|
||||
if open_explorer {
|
||||
let _ = crate::windows::open_in_explorer(drive_letter);
|
||||
}
|
||||
|
||||
// System-Tray Initialisierung
|
||||
let (tray_shutdown_tx, mut tray_shutdown_rx) = tokio::sync::mpsc::channel::<()>(1);
|
||||
#[cfg(windows)]
|
||||
let _tray = if enable_tray {
|
||||
let icon_source = crate::windows::get_default_system_icon()
|
||||
.unwrap_or(tray_item::IconSource::Resource(""));
|
||||
let title = format!("Sanctum ({drive_str})");
|
||||
match tray_item::TrayItem::new(&title, icon_source) {
|
||||
Ok(mut tray) => {
|
||||
let container_name = container_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
let _ = tray.add_label(&format!("Sanctum: {drive_str} ({container_name})"));
|
||||
let dl = drive_letter;
|
||||
let _ = tray.add_menu_item("Im Explorer öffnen", move || {
|
||||
let _ = crate::windows::open_in_explorer(dl);
|
||||
});
|
||||
let s_tx = tray_shutdown_tx.clone();
|
||||
let _ = tray.add_menu_item("Trennen & Beenden", move || {
|
||||
let _ = s_tx.blocking_send(());
|
||||
});
|
||||
Some(tray)
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("System-Tray Icon konnte nicht erstellt werden: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("┌─────────────────────────────────────────────────────────────┐");
|
||||
println!("│ ✔ Sanctum Container erfolgreich gemountet │");
|
||||
@@ -215,17 +256,25 @@ pub async fn mount_container(
|
||||
println!(" • Container: {}", container_path.display());
|
||||
println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str));
|
||||
println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port);
|
||||
if enable_tray {
|
||||
println!(" • System-Tray: Icon aktiv (Rechtsklick für Explorer/Trennen)");
|
||||
}
|
||||
println!();
|
||||
println!(" [{}] Drücke [Ctrl+C] zum sicheren Trennen und Schließen.", ui::yellow("Tipp"));
|
||||
println!(" [{}] Drücke [Ctrl+C] oder nutze das Tray-Icon zum Beenden.", ui::yellow("Tipp"));
|
||||
println!();
|
||||
|
||||
// Warten auf Strg+C
|
||||
tokio::signal::ctrl_c()
|
||||
.await
|
||||
.context("Fehler beim Registrieren des Ctrl+C Signalhandlers")?;
|
||||
|
||||
println!();
|
||||
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||
// Warten auf Strg+C ODER Signal aus dem System-Tray
|
||||
tokio::select! {
|
||||
res = tokio::signal::ctrl_c() => {
|
||||
let _ = res;
|
||||
println!();
|
||||
println!(" {} Beendigungssignal (Ctrl+C) empfangen.", ui::yellow("[!]"));
|
||||
}
|
||||
_ = tray_shutdown_rx.recv() => {
|
||||
println!();
|
||||
println!(" {} Beendigungssignal aus System-Tray empfangen.", ui::yellow("[!]"));
|
||||
}
|
||||
}
|
||||
print!(" {} Trenne Windows-Netzlaufwerk {} ... ", ui::dim("[-]"), drive_str);
|
||||
let _ = std::io::Write::flush(&mut std::io::stdout());
|
||||
|
||||
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
|
||||
/// Ermittelt den nächsten verfügbaren Windows-Laufwerksbuchstaben (von 'Z' rückwärts bis 'D').
|
||||
pub fn find_next_available_drive() -> Result<char> {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn GetLogicalDrives() -> u32;
|
||||
}
|
||||
|
||||
let mask = unsafe { GetLogicalDrives() };
|
||||
if mask == 0 {
|
||||
bail!("Fehler beim Abfragen der logischen Laufwerke via Win32 API");
|
||||
}
|
||||
|
||||
// Suche von 'Z' abwärts bis 'D' (A, B für Diskettenlaufwerke, C für System reservieren)
|
||||
for ch in ('D'..='Z').rev() {
|
||||
let bit_index = (ch as u8) - b'A';
|
||||
if (mask & (1 << bit_index)) == 0 {
|
||||
return Ok(ch);
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Kein freier Windows-Laufwerksbuchstabe (D: bis Z:) verfügbar!");
|
||||
}
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
Ok('S')
|
||||
}
|
||||
}
|
||||
|
||||
/// Öffnet das eingebundene Netzlaufwerk direkt im Windows Explorer.
|
||||
pub fn open_in_explorer(drive_char: char) -> Result<()> {
|
||||
let drive_path = format!("{}:\\", drive_char.to_ascii_uppercase());
|
||||
Command::new("explorer.exe")
|
||||
.arg(&drive_path)
|
||||
.spawn()
|
||||
.with_context(|| format!("Konnte Windows Explorer für '{}' nicht öffnen", drive_path))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Benachrichtigt die Windows-Shell (Explorer) über geänderte Dateiverknüpfungen (SHCNE_ASSOCCHANGED).
|
||||
pub fn notify_shell_associations_changed() {
|
||||
#[cfg(windows)]
|
||||
{
|
||||
extern "system" {
|
||||
fn SHChangeNotify(
|
||||
w_event_id: i32,
|
||||
u_flags: u32,
|
||||
dw_item1: *const std::ffi::c_void,
|
||||
dw_item2: *const std::ffi::c_void,
|
||||
);
|
||||
}
|
||||
|
||||
const SHCNE_ASSOCCHANGED: i32 = 0x0800_0000;
|
||||
const SHCNF_IDLIST: u32 = 0x0000;
|
||||
|
||||
unsafe {
|
||||
SHChangeNotify(
|
||||
SHCNE_ASSOCCHANGED,
|
||||
SHCNF_IDLIST,
|
||||
std::ptr::null(),
|
||||
std::ptr::null(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Registriert `.sanctum`-Containerdateien im Windows Explorer für den aktuellen Benutzer (HKCU, 100% Userland, keine Adminrechte).
|
||||
pub fn register_explorer_integration() -> Result<()> {
|
||||
let current_exe = std::env::current_exe()
|
||||
.context("Konnte den Pfad zur aktuellen sanctum.exe nicht ermitteln")?;
|
||||
let exe_str = current_exe.display().to_string();
|
||||
|
||||
let reg_commands = [
|
||||
// 1. .sanctum Erweiterung mit ProgID verknüpfen
|
||||
(
|
||||
r"HKCU\Software\Classes\.sanctum",
|
||||
"",
|
||||
"Sanctum.Container",
|
||||
),
|
||||
// 2. ProgID Metadaten & Beschreibung
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container",
|
||||
"",
|
||||
"Sanctum Verschlüsselter Container",
|
||||
),
|
||||
// 3. Icon
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\DefaultIcon",
|
||||
"",
|
||||
&format!("\"{exe_str}\",0"),
|
||||
),
|
||||
// 4. Standard-Doppelklick-Aktion: Mount
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open",
|
||||
"",
|
||||
"Als Laufwerk einbinden",
|
||||
),
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\open\command",
|
||||
"",
|
||||
&format!("\"{exe_str}\" mount --path \"%1\""),
|
||||
),
|
||||
// 5. Kontextmenü-Aktion: Integritätsprüfung
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify",
|
||||
"",
|
||||
"Integrität prüfen (FSCK)",
|
||||
),
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\verify\command",
|
||||
"",
|
||||
&format!("\"{exe_str}\" verify --path \"%1\""),
|
||||
),
|
||||
// 6. Kontextmenü-Aktion: Header sichern
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header",
|
||||
"",
|
||||
"Header sichern",
|
||||
),
|
||||
(
|
||||
r"HKCU\Software\Classes\Sanctum.Container\shell\backup_header\command",
|
||||
"",
|
||||
&format!("\"{exe_str}\" backup-header --path \"%1\""),
|
||||
),
|
||||
];
|
||||
|
||||
for (key, val_name, val_data) in reg_commands {
|
||||
let mut cmd = Command::new("reg");
|
||||
cmd.arg("add").arg(key);
|
||||
if val_name.is_empty() {
|
||||
cmd.arg("/ve");
|
||||
} else {
|
||||
cmd.arg("/v").arg(val_name);
|
||||
}
|
||||
cmd.arg("/d").arg(val_data).arg("/f");
|
||||
|
||||
let output = cmd.output().with_context(|| format!("Fehler beim Ausführen von 'reg add {key}'"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("Registry-Fehler beim Anlegen von {key}: {stderr}");
|
||||
}
|
||||
}
|
||||
|
||||
notify_shell_associations_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entfernt die Windows-Explorer-Verknüpfungen aus der Benutzer-Registry (HKCU).
|
||||
pub fn unregister_explorer_integration() -> Result<()> {
|
||||
let keys_to_delete = [
|
||||
r"HKCU\Software\Classes\.sanctum",
|
||||
r"HKCU\Software\Classes\Sanctum.Container",
|
||||
];
|
||||
|
||||
for key in keys_to_delete {
|
||||
let _ = Command::new("reg")
|
||||
.args(["delete", key, "/f"])
|
||||
.output();
|
||||
}
|
||||
|
||||
notify_shell_associations_changed();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lädt das Windows-Sicherheitsschild-Icon (IDI_SHIELD) oder Anwendungs-Icon für den System-Tray.
|
||||
#[cfg(windows)]
|
||||
pub fn get_default_system_icon() -> Option<tray_item::IconSource> {
|
||||
extern "system" {
|
||||
fn LoadIconW(instance: isize, icon_name: *const u16) -> isize;
|
||||
}
|
||||
|
||||
// IDI_SHIELD = 32518, IDI_APPLICATION = 32512
|
||||
let icon = unsafe { LoadIconW(0, 32518 as *const u16) };
|
||||
if icon != 0 {
|
||||
Some(tray_item::IconSource::RawIcon(icon))
|
||||
} else {
|
||||
let icon_app = unsafe { LoadIconW(0, 32512 as *const u16) };
|
||||
if icon_app != 0 {
|
||||
Some(tray_item::IconSource::RawIcon(icon_app))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_find_next_available_drive() {
|
||||
let drive = find_next_available_drive().expect("Find next drive");
|
||||
assert!(drive.is_ascii_alphabetic());
|
||||
assert!(drive >= 'D' && drive <= 'Z');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user