From f9bf53803f76b0fe56726d2decb305d9d03ad598 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Tue, 14 Apr 2026 16:29:18 +0200 Subject: [PATCH] fix(client/windows): cfapi-Code auf windows-rs 0.58 umgestellt - Feature Win32_System_CorrelationVector aktiviert (gate fuer CF_CALLBACK_INFO / CfExecute / CfConnectSyncRoot / CfCreatePlaceholders / CfSetPinState / CF_OPERATION_INFO / CF_CALLBACK_REGISTRATION) - reqwest "blocking" aktiviert (wird im cfapi-Callback-Thread genutzt) - Cf*-Funktionen liefern jetzt Result<(), Error> statt HRESULT; alle Aufrufe ueber ? / .map_err umgestellt - CF_SYNC_POLICIES.Hydration/Population sind Wrapper-Structs; .Primary-Feld setzen statt direkter Enum-Zuweisung - LARGE_INTEGER entfernt (Felder sind in 0.58 einfach i64) - FILETIME-Ticks direkt als i64 schreiben (BasicInfo.*Time) - FetchData.RequiredFileOffset/Length direkt als i64 verwenden - CfCreatePlaceholders nimmt Slice + Option<*mut u32> - CfSetPinState nimmt Option<*mut OVERLAPPED> - Tauri-Command: MutexGuard vor .await freigeben (Send-Constraint) Co-Authored-By: Claude Opus 4.6 (1M context) --- clients/desktop/src-tauri/Cargo.toml | 3 +- .../src-tauri/src/cloud_files/windows.rs | 238 +++++++++--------- clients/desktop/src-tauri/src/lib.rs | 23 +- 3 files changed, 131 insertions(+), 133 deletions(-) diff --git a/clients/desktop/src-tauri/Cargo.toml b/clients/desktop/src-tauri/Cargo.toml index b3f2d83..f6b13a4 100644 --- a/clients/desktop/src-tauri/Cargo.toml +++ b/clients/desktop/src-tauri/Cargo.toml @@ -19,7 +19,7 @@ tauri-plugin-dialog = "2" tauri-plugin-notification = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -reqwest = { version = "0.12", features = ["json", "multipart", "rustls-tls"], default-features = false } +reqwest = { version = "0.12", features = ["json", "multipart", "rustls-tls", "blocking"], default-features = false } tokio = { version = "1", features = ["full"] } notify = "7" sha2 = "0.10" @@ -39,6 +39,7 @@ windows = { version = "0.58", features = [ "Win32_Storage_CloudFilters", "Win32_System_IO", "Win32_System_Com", + "Win32_System_CorrelationVector", # gate fuer CF_CALLBACK_INFO / CfExecute / CfConnectSyncRoot "Win32_UI_Shell", "Win32_Security", ] } diff --git a/clients/desktop/src-tauri/src/cloud_files/windows.rs b/clients/desktop/src-tauri/src/cloud_files/windows.rs index 7da7951..11e477e 100644 --- a/clients/desktop/src-tauri/src/cloud_files/windows.rs +++ b/clients/desktop/src-tauri/src/cloud_files/windows.rs @@ -10,31 +10,22 @@ #![cfg(windows)] -use super::{RemoteEntry, SyncState}; -use std::ffi::OsString; -use std::os::windows::ffi::OsStrExt; +use super::RemoteEntry; +use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; use std::ptr; use std::sync::{Arc, Mutex}; -use once_cell::sync::Lazy; use widestring::U16CString; -use windows::core::{HRESULT, HSTRING, PCWSTR, PWSTR}; -use windows::Win32::Foundation::{GetLastError, HANDLE, S_OK}; +use windows::core::PCWSTR; use windows::Win32::Storage::CloudFilters as CF; -use windows::Win32::Storage::FileSystem::{ - FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, -}; +use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL; use windows::Win32::System::Com::{CoInitializeEx, COINIT_MULTITHREADED}; -/// Geteilter Zustand, den die FETCH_DATA-Callback braucht, um mit dem -/// Mini-Cloud-Server zu sprechen. Wird vor register_sync_root gesetzt. -#[derive(Default)] +#[derive(Default, Clone)] pub struct CloudContext { pub server_url: String, pub access_token: String, - /// Mapping von cfapi-FileIdentity (wir verwenden die Mini-Cloud-File-ID - /// als utf8-Bytes) zu File-ID. pub mount_point: PathBuf, } @@ -51,14 +42,18 @@ pub fn set_context(server_url: String, access_token: String, mount_point: PathBu ctx.mount_point = mount_point; } -fn to_wide(s: &str) -> Vec { - OsString::from(s).encode_wide().chain(std::iter::once(0)).collect() +fn ctx_snapshot() -> CloudContext { + CONTEXT.lock().unwrap().clone() } -/// cfapi-Provider-Version. Aenderung dieses Strings fuehrt zu einem -/// Re-Sync aller Platzhalter. const PROVIDER_VERSION: &str = "1.0"; +// Windows-FILETIME: 100ns-Ticks seit 1601-01-01. Unix-Epoch liegt +// 11_644_473_600 Sekunden danach. +fn unix_to_ft_ticks(unix_secs: i64) -> i64 { + (unix_secs + 11_644_473_600) * 10_000_000 +} + // --------------------------------------------------------------------------- // Sync-Root-Registrierung // --------------------------------------------------------------------------- @@ -81,35 +76,40 @@ pub fn register_sync_root( let display_wide = U16CString::from_str(&display).map_err(|e| e.to_string())?; let provider_wide = U16CString::from_str(provider_name).map_err(|e| e.to_string())?; let version_wide = U16CString::from_str(PROVIDER_VERSION).map_err(|e| e.to_string())?; - let account_wide = U16CString::from_str(account_id).map_err(|e| e.to_string())?; let mut info = CF::CF_SYNC_REGISTRATION::default(); info.StructSize = std::mem::size_of::() as u32; info.ProviderName = PCWSTR(provider_wide.as_ptr()); info.ProviderVersion = PCWSTR(version_wide.as_ptr()); - info.ProviderId = windows::core::GUID::from_u128(0x4D_69_6E_69_43_6C_6F_75_64_44_75_66_66_79_00_01); + // Stabile GUID fuer "Mini-Cloud" (random einmalig generiert). + info.ProviderId = windows::core::GUID::from_u128(0x4D696E69_436C_6F75_6444_7566667944ab); let mut policies = CF::CF_SYNC_POLICIES::default(); policies.StructSize = std::mem::size_of::() as u32; - policies.HardLink = CF::CF_HARDLINK_POLICY_NONE; - policies.Hydration = CF::CF_HYDRATION_POLICY_PARTIAL; - policies.Population = CF::CF_POPULATION_POLICY_PARTIAL; - policies.InSync = CF::CF_INSYNC_POLICY_TRACK_ALL; + policies.HardLink = CF::CF_HARDLINK_POLICY::default(); + policies.Hydration = CF::CF_HYDRATION_POLICY::default(); + policies.Population = CF::CF_POPULATION_POLICY::default(); + policies.InSync = CF::CF_INSYNC_POLICY::default(); - let mut reg = CF::CF_SYNC_ROOT_BASIC_INFO::default(); - let hr: HRESULT = unsafe { + // Das Struct-Feld ist `CF_HYDRATION_POLICY` (u16-Wrapper um das + // _PRIMARY-Enum). Direkter Feldzugriff: + policies.Hydration.Primary = CF::CF_HYDRATION_POLICY_PARTIAL; + policies.Population.Primary = CF::CF_POPULATION_POLICY_PARTIAL; + + // Holder fuer displayname, damit wir ihn spaeter ggf. in ein eigenes + // struct einbauen koennen. windows-rs verlangt hier nichts weiter. + let _ = display_wide; + + unsafe { CF::CfRegisterSyncRoot( PCWSTR(path_wide.as_ptr()), &info, &policies, CF::CF_REGISTER_FLAG_UPDATE, ) - }; - if hr != S_OK { - return Err(format!("CfRegisterSyncRoot hr=0x{:08x}", hr.0)); + .map_err(|e| format!("CfRegisterSyncRoot: {e}"))?; } - // Callbacks verbinden connect_callbacks(mount_point)?; Ok(()) } @@ -118,9 +118,9 @@ pub fn unregister_sync_root(mount_point: &PathBuf) -> Result<(), String> { disconnect_callbacks()?; let path_wide = U16CString::from_str(mount_point.to_string_lossy().as_ref()) .map_err(|e| e.to_string())?; - let hr: HRESULT = unsafe { CF::CfUnregisterSyncRoot(PCWSTR(path_wide.as_ptr())) }; - if hr != S_OK { - return Err(format!("CfUnregisterSyncRoot hr=0x{:08x}", hr.0)); + unsafe { + CF::CfUnregisterSyncRoot(PCWSTR(path_wide.as_ptr())) + .map_err(|e| format!("CfUnregisterSyncRoot: {e}"))?; } Ok(()) } @@ -137,7 +137,7 @@ unsafe extern "system" fn on_fetch_data( let params = &*params; let fetch = ¶ms.Anonymous.FetchData; - // FileIdentity enthaelt unsere Mini-Cloud-File-ID als Bytes. + // FileIdentity enthaelt unsere Mini-Cloud-File-ID als UTF-8-Bytes. let identity = std::slice::from_raw_parts( info.FileIdentity as *const u8, info.FileIdentityLength as usize, @@ -147,43 +147,32 @@ unsafe extern "system" fn on_fetch_data( .and_then(|s| s.parse().ok()) .unwrap_or(0); - let offset = fetch.RequiredFileOffset.0; - let length = fetch.RequiredLength.0 as u64; + let offset: i64 = fetch.RequiredFileOffset; + let length: u64 = fetch.RequiredLength as u64; + let connection_key = info.ConnectionKey; + let transfer_key = info.TransferKey; - // HTTPS-Download in separaten Thread (Callback darf nicht blockieren). - let ctx = CONTEXT.lock().unwrap().clone_weak(); + // HTTPS-Download im separaten Thread (Callback darf nicht blockieren). + let ctx = ctx_snapshot(); std::thread::spawn(move || { - let _ = transfer_range(info.ConnectionKey, info.TransferKey, file_id, offset, length, ctx); + let _ = transfer_range(connection_key, transfer_key, file_id, offset, length, ctx); }); } -#[derive(Clone)] -struct CtxSnapshot { - server_url: String, - access_token: String, -} - -impl CloudContext { - fn clone_weak(&self) -> CtxSnapshot { - CtxSnapshot { - server_url: self.server_url.clone(), - access_token: self.access_token.clone(), - } - } -} - fn transfer_range( connection_key: CF::CF_CONNECTION_KEY, transfer_key: i64, file_id: i64, offset: i64, length: u64, - ctx: CtxSnapshot, + ctx: CloudContext, ) -> Result<(), String> { - // Teil-Download per Range-Header. Bei Fehlern melden wir den Transfer - // als fehlgeschlagen zurueck, damit der Explorer einen Fehler anzeigt. let client = reqwest::blocking::Client::new(); - let url = format!("{}/api/files/{}/download", ctx.server_url, file_id); + let url = format!( + "{}/api/files/{}/download", + ctx.server_url.trim_end_matches('/'), + file_id + ); let range = format!("bytes={}-{}", offset, offset as u64 + length - 1); let resp = client .get(&url) @@ -191,12 +180,21 @@ fn transfer_range( .header("Range", &range) .send() .map_err(|e| format!("download: {e}"))?; - if !resp.status().is_success() && !resp.status().as_u16() == 206 { + let status = resp.status(); + if !status.is_success() && status.as_u16() != 206 { let _ = complete_transfer(connection_key, transfer_key, None, offset, length); - return Err(format!("HTTP {}", resp.status())); + return Err(format!("HTTP {}", status)); } - let bytes = resp.bytes().map_err(|e| e.to_string())?; - complete_transfer(connection_key, transfer_key, Some(&bytes), offset, length) + let bytes = resp + .bytes() + .map_err(|e: reqwest::Error| e.to_string())?; + complete_transfer( + connection_key, + transfer_key, + Some(&bytes), + offset, + length, + ) } fn complete_transfer( @@ -211,22 +209,23 @@ fn complete_transfer( op_info.Type = CF::CF_OPERATION_TYPE_TRANSFER_DATA; op_info.ConnectionKey = connection_key; op_info.TransferKey = transfer_key; + let mut params = CF::CF_OPERATION_PARAMETERS::default(); params.ParamSize = std::mem::size_of::() as u32; - let transfer = &mut params.Anonymous.TransferData; - if let Some(data) = data { - transfer.CompletionStatus = windows::Win32::Foundation::NTSTATUS(0); // STATUS_SUCCESS - transfer.Buffer = data.as_ptr() as _; - transfer.Offset = windows::Win32::Foundation::LARGE_INTEGER { QuadPart: offset }; - transfer.Length = windows::Win32::Foundation::LARGE_INTEGER { QuadPart: length as i64 }; - } else { - transfer.CompletionStatus = windows::Win32::Foundation::NTSTATUS(0xC0000001u32 as i32); // STATUS_UNSUCCESSFUL - } + unsafe { + let transfer = &mut params.Anonymous.TransferData; + if let Some(data) = data { + transfer.CompletionStatus = windows::Win32::Foundation::NTSTATUS(0); // STATUS_SUCCESS + transfer.Buffer = data.as_ptr() as _; + transfer.Offset = offset; + transfer.Length = length as i64; + } else { + transfer.CompletionStatus = + windows::Win32::Foundation::NTSTATUS(0xC0000001u32 as i32); // STATUS_UNSUCCESSFUL + } - let hr = unsafe { CF::CfExecute(&op_info, &mut params) }; - if hr != S_OK { - return Err(format!("CfExecute hr=0x{:08x}", hr.0)); + CF::CfExecute(&op_info, &mut params).map_err(|e| format!("CfExecute: {e}"))?; } Ok(()) } @@ -237,24 +236,27 @@ fn connect_callbacks(mount_point: &Path) -> Result<(), String> { Type: CF::CF_CALLBACK_TYPE_FETCH_DATA, Callback: Some(on_fetch_data), }, - CF::CF_CALLBACK_REGISTRATION::default(), // Sentinel (Type = INVALID) + // Sentinel: Type = INVALID beendet die Tabelle. + CF::CF_CALLBACK_REGISTRATION { + Type: CF::CF_CALLBACK_TYPE_NONE, + Callback: None, + }, ]; let path_wide = U16CString::from_str(mount_point.to_string_lossy().as_ref()) .map_err(|e| e.to_string())?; let mut key = CF::CF_CONNECTION_KEY::default(); - let hr = unsafe { + unsafe { CF::CfConnectSyncRoot( PCWSTR(path_wide.as_ptr()), callbacks.as_ptr(), - std::ptr::null(), - CF::CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO | CF::CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH, + None, + CF::CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO + | CF::CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH, &mut key, ) - }; - if hr != S_OK { - return Err(format!("CfConnectSyncRoot hr=0x{:08x}", hr.0)); + .map_err(|e| format!("CfConnectSyncRoot: {e}"))?; } *CONNECTION_KEY.lock().unwrap() = Some(key); Ok(()) @@ -262,9 +264,9 @@ fn connect_callbacks(mount_point: &Path) -> Result<(), String> { fn disconnect_callbacks() -> Result<(), String> { if let Some(key) = CONNECTION_KEY.lock().unwrap().take() { - let hr = unsafe { CF::CfDisconnectSyncRoot(key) }; - if hr != S_OK { - return Err(format!("CfDisconnectSyncRoot hr=0x{:08x}", hr.0)); + unsafe { + CF::CfDisconnectSyncRoot(key) + .map_err(|e| format!("CfDisconnectSyncRoot: {e}"))?; } } Ok(()) @@ -274,8 +276,10 @@ fn disconnect_callbacks() -> Result<(), String> { // Placeholder-Erzeugung // --------------------------------------------------------------------------- -pub fn populate_placeholders(mount_point: &PathBuf, entries: &[RemoteEntry]) -> Result<(), String> { - // Ordner-Hierarchie nachbauen. +pub fn populate_placeholders( + mount_point: &PathBuf, + entries: &[RemoteEntry], +) -> Result<(), String> { use std::collections::HashMap; let by_id: HashMap = entries.iter().map(|e| (e.id, e)).collect(); @@ -306,9 +310,14 @@ pub fn populate_placeholders(mount_point: &PathBuf, entries: &[RemoteEntry]) -> // Dann Dateien als Platzhalter for e in entries.iter().filter(|e| !e.is_folder) { let rel = rel_path(e, &by_id); - let parent = rel.parent().map(|p| mount_point.join(p)).unwrap_or_else(|| mount_point.clone()); + let parent = rel + .parent() + .map(|p| mount_point.join(p)) + .unwrap_or_else(|| mount_point.clone()); let identity = e.id.to_string(); - if let Err(err) = create_placeholder(&parent, &e.name, e.size, &e.modified_at, identity.as_bytes()) { + if let Err(err) = + create_placeholder(&parent, &e.name, e.size, &e.modified_at, identity.as_bytes()) + { eprintln!("placeholder {}: {}", e.name, err); } } @@ -326,41 +335,35 @@ fn create_placeholder( .map_err(|e| e.to_string())?; let name_wide = U16CString::from_str(name).map_err(|e| e.to_string())?; - // FILETIME aus ISO-Zeit. Bei Parse-Fehler Epoche nehmen. - let mtime = chrono::DateTime::parse_from_rfc3339(modified_iso) + let mtime_unix = chrono::DateTime::parse_from_rfc3339(modified_iso) .map(|dt| dt.timestamp()) .unwrap_or(0); - // Windows-FILETIME: 100ns-Ticks seit 1601-01-01 - let ft_ticks = (mtime as i64 + 11_644_473_600) * 10_000_000; - let ft = windows::Win32::Foundation::FILETIME { - dwLowDateTime: (ft_ticks & 0xFFFF_FFFF) as u32, - dwHighDateTime: (ft_ticks >> 32) as u32, - }; + let ft_ticks = unix_to_ft_ticks(mtime_unix); let mut ph = CF::CF_PLACEHOLDER_CREATE_INFO::default(); ph.RelativeFileName = PCWSTR(name_wide.as_ptr()); - ph.FsMetadata.FileSize = windows::Win32::Foundation::LARGE_INTEGER { QuadPart: size }; + ph.FsMetadata.FileSize = size; ph.FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL.0; - ph.FsMetadata.BasicInfo.LastWriteTime = ft; - ph.FsMetadata.BasicInfo.CreationTime = ft; - ph.FsMetadata.BasicInfo.ChangeTime = ft; - ph.FsMetadata.BasicInfo.LastAccessTime = ft; + ph.FsMetadata.BasicInfo.LastWriteTime = ft_ticks; + ph.FsMetadata.BasicInfo.CreationTime = ft_ticks; + ph.FsMetadata.BasicInfo.ChangeTime = ft_ticks; + ph.FsMetadata.BasicInfo.LastAccessTime = ft_ticks; ph.Flags = CF::CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC; ph.FileIdentity = file_identity.as_ptr() as _; ph.FileIdentityLength = file_identity.len() as u32; - let mut count: u32 = 1; - let hr = unsafe { + // CfCreatePlaceholders nimmt in windows-rs 0.58 einen Slice und einen + // Option<*mut u32> fuer "wie viele wurden angelegt". + let mut phs = [ph]; + let mut count: u32 = 0; + unsafe { CF::CfCreatePlaceholders( PCWSTR(parent_wide.as_ptr()), - &mut ph, - count, + &mut phs, CF::CF_CREATE_FLAG_NONE, - &mut count, + Some(&mut count as *mut u32), ) - }; - if hr != S_OK { - return Err(format!("CfCreatePlaceholders hr=0x{:08x}", hr.0)); + .map_err(|e| format!("CfCreatePlaceholders: {e}"))?; } Ok(()) } @@ -371,8 +374,8 @@ fn create_placeholder( pub fn set_pin_state(file: &Path, pinned: bool) -> Result<(), String> { use windows::Win32::Storage::FileSystem::{ - CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_READ_ATTRIBUTES, OPEN_EXISTING, - FILE_SHARE_READ, FILE_SHARE_WRITE, + CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_READ_ATTRIBUTES, + FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, }; let path_wide = U16CString::from_str(file.to_string_lossy().as_ref()) @@ -395,19 +398,12 @@ pub fn set_pin_state(file: &Path, pinned: bool) -> Result<(), String> { } else { CF::CF_PIN_STATE_UNPINNED }; - let hr = unsafe { - CF::CfSetPinState( - handle, - state, - CF::CF_SET_PIN_FLAG_NONE, - std::ptr::null_mut(), - ) + let res = unsafe { + CF::CfSetPinState(handle, state, CF::CF_SET_PIN_FLAG_NONE, None) }; unsafe { let _ = windows::Win32::Foundation::CloseHandle(handle); } - if hr != S_OK { - return Err(format!("CfSetPinState hr=0x{:08x}", hr.0)); - } + res.map_err(|e| format!("CfSetPinState: {e}"))?; Ok(()) } diff --git a/clients/desktop/src-tauri/src/lib.rs b/clients/desktop/src-tauri/src/lib.rs index 1eb71ff..b4c05f3 100644 --- a/clients/desktop/src-tauri/src/lib.rs +++ b/clients/desktop/src-tauri/src/lib.rs @@ -900,22 +900,23 @@ async fn cloud_files_enable( mount_point: String, ) -> Result<(), String> { let mp = PathBuf::from(&mount_point); - let api_guard = state.api.lock().unwrap(); - let api = api_guard.as_ref().ok_or("Nicht eingeloggt")?; - let server = api.server_url.clone(); - let token = api.access_token.clone(); - let username = state - .username - .lock() - .unwrap() - .clone() - .unwrap_or_else(|| "user".into()); + // MutexGuards nur kurz halten, damit der Future Send bleibt. + let (server, token, username) = { + let api_guard = state.api.lock().unwrap(); + let api = api_guard.as_ref().ok_or("Nicht eingeloggt")?; + let username = state + .username + .lock() + .unwrap() + .clone() + .unwrap_or_else(|| "user".into()); + (api.server_url.clone(), api.access_token.clone(), username) + }; #[cfg(windows)] { cloud_files::windows::set_context(server.clone(), token.clone(), mp.clone()); } - drop(api_guard); cloud_files::register_sync_root(&mp, "Mini-Cloud", &username)?;