//! Windows Cloud Files API Integration. //! //! Registriert den Sync-Ordner als Sync-Root, legt Platzhalter-Dateien an //! und reicht Zugriffe auf Dateidaten als HTTPS-Download durch. Der //! Explorer zeigt Wolken-/Haken-Overlays automatisch an, solange die //! Pin-Stati korrekt gesetzt sind. //! //! Voraussetzung: Windows 10 1709+ (cfapi.dll). Der Account-Identifier //! sollte stabil sein (z.B. Hash(Server-URL + Username)). #![cfg(windows)] use super::RemoteEntry; use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; use std::ptr; use std::sync::{Arc, Mutex}; use widestring::U16CString; use windows::core::PCWSTR; use windows::Win32::Storage::CloudFilters as CF; use windows::Win32::Storage::FileSystem::FILE_ATTRIBUTE_NORMAL; use windows::Win32::System::Com::{CoInitializeEx, COINIT_MULTITHREADED}; #[derive(Default, Clone)] pub struct CloudContext { pub server_url: String, pub access_token: String, pub mount_point: PathBuf, } static CONTEXT: Lazy>> = Lazy::new(|| Arc::new(Mutex::new(CloudContext::default()))); static CONNECTION_KEY: Lazy>> = Lazy::new(|| Mutex::new(None)); pub fn set_context(server_url: String, access_token: String, mount_point: PathBuf) { let mut ctx = CONTEXT.lock().unwrap(); ctx.server_url = server_url; ctx.access_token = access_token; ctx.mount_point = mount_point; } fn ctx_snapshot() -> CloudContext { CONTEXT.lock().unwrap().clone() } 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 // --------------------------------------------------------------------------- pub fn register_sync_root( mount_point: &PathBuf, provider_name: &str, account_id: &str, ) -> Result<(), String> { // COM initialisieren (cfapi benoetigt MTA-Apartment) unsafe { let _ = CoInitializeEx(Some(ptr::null()), COINIT_MULTITHREADED); } std::fs::create_dir_all(mount_point).map_err(|e| format!("mkdir: {e}"))?; let display = format!("Mini-Cloud - {}", account_id); let path_wide = U16CString::from_str(mount_point.to_string_lossy().as_ref()) .map_err(|e| format!("path encode: {e}"))?; 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 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()); // 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::default(); policies.Hydration = CF::CF_HYDRATION_POLICY::default(); policies.Population = CF::CF_POPULATION_POLICY::default(); policies.InSync = CF::CF_INSYNC_POLICY::default(); // 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; // Erst versuchen ohne UPDATE-Flag (frische Registrierung). Bei "schon // registriert"-Fehler nochmal mit UPDATE-Flag. So laeuft es beim ersten // Aktivieren UND bei Folgestarts. unsafe { if let Err(e) = CF::CfRegisterSyncRoot( PCWSTR(path_wide.as_ptr()), &info, &policies, CF::CF_REGISTER_FLAG_NONE, ) { let already = e.code().0 == 0x80070091u32 as i32 // DIR_NOT_EMPTY || e.code().0 == 0x800710D1u32 as i32; // Already registered if already { CF::CfRegisterSyncRoot( PCWSTR(path_wide.as_ptr()), &info, &policies, CF::CF_REGISTER_FLAG_UPDATE, ) .map_err(|e| format!("CfRegisterSyncRoot(UPDATE): {e}"))?; } else { return Err(format!("CfRegisterSyncRoot: {e}")); } } } connect_callbacks(mount_point)?; Ok(()) } 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())?; unsafe { CF::CfUnregisterSyncRoot(PCWSTR(path_wide.as_ptr())) .map_err(|e| format!("CfUnregisterSyncRoot: {e}"))?; } Ok(()) } // --------------------------------------------------------------------------- // Callback-Tabelle // --------------------------------------------------------------------------- unsafe extern "system" fn on_fetch_data( info: *const CF::CF_CALLBACK_INFO, params: *const CF::CF_CALLBACK_PARAMETERS, ) { let info = &*info; let params = &*params; let fetch = ¶ms.Anonymous.FetchData; // 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, ); let file_id: i64 = std::str::from_utf8(identity) .ok() .and_then(|s| s.parse().ok()) .unwrap_or(0); let offset: i64 = fetch.RequiredFileOffset; let length: u64 = fetch.RequiredLength as u64; let connection_key = info.ConnectionKey; let transfer_key = info.TransferKey; // HTTPS-Download im separaten Thread (Callback darf nicht blockieren). let ctx = ctx_snapshot(); std::thread::spawn(move || { log_msg(&ctx.mount_point, &format!( "FETCH_DATA file_id={file_id} offset={offset} len={length}" )); match transfer_range(connection_key, transfer_key, file_id, offset, length, &ctx) { Ok(()) => log_msg(&ctx.mount_point, &format!( "fetch file_id={file_id} OK" )), Err(e) => { log_err(&ctx.mount_point, &format!( "fetch file_id={file_id} offset={offset} len={length} FAILED: {e}" )); // Garantiert Fehler-Completion, damit Windows nicht in Timeout laeuft. let _ = complete_transfer(connection_key, transfer_key, None, offset, length); } } }); } pub fn log_msg(mount: &Path, msg: &str) { use std::io::Write; // Log-Datei NEBEN den Mount, damit sie nicht selbst als Platzhalter // behandelt wird. let log = mount .parent() .map(|p| p.join(".minicloud-cloudfiles.log")) .unwrap_or_else(|| PathBuf::from(".minicloud-cloudfiles.log")); if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&log) { let _ = writeln!(f, "[{}] {}", chrono::Utc::now().to_rfc3339(), msg); } } fn log_err(mount: &Path, msg: &str) { log_msg(mount, msg); } /// True wenn die Datei ein cfapi-Platzhalter ist (noch nicht hydriert) /// oder gerade vom Cloud-Filter verwaltet wird. Fuer solche Dateien /// duerfen wir KEINEN Upload ausloesen, sonst verwandelt der Sync-Loop /// jeden Platzhalter sofort in eine vollstaendig lokale Datei. pub fn is_cfapi_placeholder(path: &Path) -> bool { use windows::Win32::Storage::FileSystem::GetFileAttributesW; let Ok(w) = U16CString::from_str(path.to_string_lossy().as_ref()) else { return false; }; let attrs = unsafe { GetFileAttributesW(PCWSTR(w.as_ptr())) }; if attrs == u32::MAX { return false; } // FILE_ATTRIBUTE_OFFLINE (0x1000) oder // FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS (0x400000) oder // FILE_ATTRIBUTE_RECALL_ON_OPEN (0x40000) (attrs & 0x0040_1000) != 0 || (attrs & 0x0004_0000) != 0 } fn transfer_range( connection_key: CF::CF_CONNECTION_KEY, transfer_key: i64, file_id: i64, offset: i64, length: u64, ctx: &CloudContext, ) -> Result<(), String> { if ctx.server_url.is_empty() || ctx.access_token.is_empty() { return Err("CloudContext nicht gesetzt (Server/Token leer)".into()); } let client = reqwest::blocking::Client::builder() .timeout(std::time::Duration::from_secs(60)) .build() .map_err(|e| format!("client: {e}"))?; 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) .bearer_auth(&ctx.access_token) .header("Range", &range) .send() .map_err(|e| format!("send: {e}"))?; let status = resp.status(); if !status.is_success() && status.as_u16() != 206 { return Err(format!("HTTP {}", status)); } let bytes = resp.bytes().map_err(|e: reqwest::Error| e.to_string())?; // Wenn Server kein Range unterstuetzt und volle Datei liefert, // aus dem Body den angeforderten Bereich ausschneiden. let slice: &[u8] = if status.as_u16() == 206 { &bytes[..] } else { let start = offset as usize; let end = (start + length as usize).min(bytes.len()); if start >= bytes.len() { &[] } else { &bytes[start..end] } }; complete_transfer(connection_key, transfer_key, Some(slice), offset, slice.len() as u64) } fn complete_transfer( connection_key: CF::CF_CONNECTION_KEY, transfer_key: i64, data: Option<&[u8]>, offset: i64, length: u64, ) -> Result<(), String> { let mut op_info = CF::CF_OPERATION_INFO::default(); op_info.StructSize = std::mem::size_of::() as u32; 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; 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 } CF::CfExecute(&op_info, &mut params).map_err(|e| format!("CfExecute: {e}"))?; } Ok(()) } fn connect_callbacks(mount_point: &Path) -> Result<(), String> { let callbacks = [ CF::CF_CALLBACK_REGISTRATION { Type: CF::CF_CALLBACK_TYPE_FETCH_DATA, Callback: Some(on_fetch_data), }, // 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 key = unsafe { CF::CfConnectSyncRoot( PCWSTR(path_wide.as_ptr()), callbacks.as_ptr(), None, CF::CF_CONNECT_FLAG_REQUIRE_PROCESS_INFO | CF::CF_CONNECT_FLAG_REQUIRE_FULL_FILE_PATH, ) .map_err(|e| format!("CfConnectSyncRoot: {e}"))? }; *CONNECTION_KEY.lock().unwrap() = Some(key); Ok(()) } fn disconnect_callbacks() -> Result<(), String> { if let Some(key) = CONNECTION_KEY.lock().unwrap().take() { unsafe { CF::CfDisconnectSyncRoot(key) .map_err(|e| format!("CfDisconnectSyncRoot: {e}"))?; } } Ok(()) } // --------------------------------------------------------------------------- // Placeholder-Erzeugung // --------------------------------------------------------------------------- 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(); fn rel_path<'a>( entry: &'a RemoteEntry, by_id: &HashMap, ) -> PathBuf { let mut parts = vec![entry.name.as_str()]; let mut cur = entry.parent_id; while let Some(id) = cur { if let Some(p) = by_id.get(&id) { parts.push(p.name.as_str()); cur = p.parent_id; } else { break; } } parts.reverse(); parts.iter().collect() } // Erst Ordner anlegen for e in entries.iter().filter(|e| e.is_folder) { let p = mount_point.join(rel_path(e, &by_id)); std::fs::create_dir_all(&p).ok(); } // 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 identity = e.id.to_string(); if let Err(err) = create_placeholder(&parent, &e.name, e.size, &e.modified_at, identity.as_bytes()) { eprintln!("placeholder {}: {}", e.name, err); } } Ok(()) } pub fn create_placeholder_at( parent_dir: &Path, name: &str, size: i64, modified_iso: &str, file_identity: &[u8], ) -> Result<(), String> { create_placeholder(parent_dir, name, size, modified_iso, file_identity) } fn create_placeholder( parent_dir: &Path, name: &str, size: i64, modified_iso: &str, file_identity: &[u8], ) -> Result<(), String> { let parent_wide = U16CString::from_str(parent_dir.to_string_lossy().as_ref()) .map_err(|e| e.to_string())?; let name_wide = U16CString::from_str(name).map_err(|e| e.to_string())?; let mtime_unix = chrono::DateTime::parse_from_rfc3339(modified_iso) .map(|dt| dt.timestamp()) .unwrap_or(0); 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 = size; ph.FsMetadata.BasicInfo.FileAttributes = FILE_ATTRIBUTE_NORMAL.0; 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; // 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 phs, CF::CF_CREATE_FLAG_NONE, Some(&mut count as *mut u32), ) .map_err(|e| format!("CfCreatePlaceholders: {e}"))?; } Ok(()) } // --------------------------------------------------------------------------- // Pin / Unpin (offline halten) // --------------------------------------------------------------------------- pub fn set_pin_state(file: &Path, pinned: bool) -> Result<(), String> { use windows::Win32::Storage::FileSystem::{ 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()) .map_err(|e| e.to_string())?; let handle = unsafe { CreateFileW( PCWSTR(path_wide.as_ptr()), FILE_READ_ATTRIBUTES.0, FILE_SHARE_READ | FILE_SHARE_WRITE, None, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, None, ) } .map_err(|e| format!("open: {e}"))?; let state = if pinned { CF::CF_PIN_STATE_PINNED } else { CF::CF_PIN_STATE_UNPINNED }; let res = unsafe { CF::CfSetPinState(handle, state, CF::CF_SET_PIN_FLAG_NONE, None) }; unsafe { let _ = windows::Win32::Foundation::CloseHandle(handle); } res.map_err(|e| format!("CfSetPinState: {e}"))?; Ok(()) }