3c340f9653
set_pin_state hatte drei Probleme: - FILE_READ_ATTRIBUTES: CfSetPinState braucht WRITE_ATTRIBUTES - Kein OPEN_REPARSE_POINT: das Oeffnen selbst hat evtl. die Hydration getriggert, bevor wir unpinnen konnten - Kein CfDehydratePlaceholder: Pin-Wechsel auf UNPINNED aendert nur das Flag, der Disk-Space wird nicht freigegeben Jetzt: - WRITE_ATTRIBUTES + OPEN_REPARSE_POINT beim Handle-Oeffnen - Bei Unpin zusaetzlich CfDehydratePlaceholder, damit "Speicher freigeben" auch wirklich Platz freiraeumt - Ergebnis + Fehler werden nach <parent>\.minicloud-cloudfiles.log geschrieben, damit wir sehen was passiert handle_cli_shortcuts loggt nun nach %LOCALAPPDATA%\MiniCloud Sync\ cli.log, weil Explorer die stdout/stderr eines gestarteten Prozesses verwirft. Ohne das Log kann man die vom Kontextmenue gestarteten Aktionen nicht debuggen. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
587 lines
21 KiB
Rust
587 lines
21 KiB
Rust
//! 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<Arc<Mutex<CloudContext>>> =
|
|
Lazy::new(|| Arc::new(Mutex::new(CloudContext::default())));
|
|
|
|
static CONNECTION_KEY: Lazy<Mutex<Option<CF::CF_CONNECTION_KEY>>> =
|
|
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::<CF::CF_SYNC_REGISTRATION>() 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::<CF::CF_SYNC_POLICIES>() 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();
|
|
|
|
// Hydration PARTIAL = Datei-Inhalt kommt bei Zugriff per FETCH_DATA.
|
|
// Population FULL = Ordnerinhalt ist komplett vorgefuellt durch uns
|
|
// (populate_placeholders). So muss Windows NICHT FETCH_PLACEHOLDERS
|
|
// callen, den wir nicht implementieren - sonst timeout beim Oeffnen.
|
|
policies.Hydration.Primary = CF::CF_HYDRATION_POLICY_PARTIAL;
|
|
policies.Population.Primary = CF::CF_POPULATION_POLICY_FULL;
|
|
|
|
// Holder fuer displayname, damit wir ihn spaeter ggf. in ein eigenes
|
|
// struct einbauen koennen. windows-rs verlangt hier nichts weiter.
|
|
let _ = display_wide;
|
|
|
|
// Erst eine eventuell vorhandene Registrierung wegraeumen. Sonst
|
|
// uebernimmt UPDATE nur einen Teil der Policies und alte PARTIAL-
|
|
// Population-Einstellungen bleiben aktiv -> Explorer-Timeout.
|
|
unsafe {
|
|
let _ = CF::CfUnregisterSyncRoot(PCWSTR(path_wide.as_ptr()));
|
|
}
|
|
|
|
log_msg(mount_point, &format!(
|
|
"register_sync_root path={} provider={} account={}",
|
|
mount_point.display(), provider_name, account_id
|
|
));
|
|
|
|
unsafe {
|
|
if let Err(e) = CF::CfRegisterSyncRoot(
|
|
PCWSTR(path_wide.as_ptr()),
|
|
&info,
|
|
&policies,
|
|
CF::CF_REGISTER_FLAG_NONE,
|
|
) {
|
|
log_err(mount_point, &format!("CfRegisterSyncRoot FAILED: {e:?}"));
|
|
// Als Fallback mit UPDATE-Flag
|
|
CF::CfRegisterSyncRoot(
|
|
PCWSTR(path_wide.as_ptr()),
|
|
&info,
|
|
&policies,
|
|
CF::CF_REGISTER_FLAG_UPDATE,
|
|
)
|
|
.map_err(|e| format!("CfRegisterSyncRoot(UPDATE): {e}"))?;
|
|
}
|
|
}
|
|
|
|
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> {
|
|
// 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 {
|
|
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::<CF::CF_OPERATION_INFO>() 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::<CF::CF_OPERATION_PARAMETERS>() 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(())
|
|
}
|
|
|
|
unsafe extern "system" fn on_fetch_placeholders(
|
|
info: *const CF::CF_CALLBACK_INFO,
|
|
_params: *const CF::CF_CALLBACK_PARAMETERS,
|
|
) {
|
|
// Safety-Net: wir populieren schon ueber populate_placeholders,
|
|
// aber falls Windows trotzdem ruft, geben wir leere Antwort.
|
|
let info = &*info;
|
|
let mut op_info = CF::CF_OPERATION_INFO::default();
|
|
op_info.StructSize = std::mem::size_of::<CF::CF_OPERATION_INFO>() as u32;
|
|
op_info.Type = CF::CF_OPERATION_TYPE_TRANSFER_PLACEHOLDERS;
|
|
op_info.ConnectionKey = info.ConnectionKey;
|
|
op_info.TransferKey = info.TransferKey;
|
|
let mut params = CF::CF_OPERATION_PARAMETERS::default();
|
|
params.ParamSize = std::mem::size_of::<CF::CF_OPERATION_PARAMETERS>() as u32;
|
|
let transfer = &mut params.Anonymous.TransferPlaceholders;
|
|
transfer.CompletionStatus = windows::Win32::Foundation::NTSTATUS(0);
|
|
transfer.PlaceholderTotalCount = 0;
|
|
transfer.PlaceholderArray = std::ptr::null_mut();
|
|
transfer.PlaceholderCount = 0;
|
|
transfer.EntriesProcessed = 0;
|
|
transfer.Flags = CF::CF_OPERATION_TRANSFER_PLACEHOLDERS_FLAG_DISABLE_ON_DEMAND_POPULATION;
|
|
let _ = CF::CfExecute(&op_info, &mut params);
|
|
}
|
|
|
|
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),
|
|
},
|
|
CF::CF_CALLBACK_REGISTRATION {
|
|
Type: CF::CF_CALLBACK_TYPE_FETCH_PLACEHOLDERS,
|
|
Callback: Some(on_fetch_placeholders),
|
|
},
|
|
// 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;
|
|
log_msg(mount_point, &format!(
|
|
"populate_placeholders: {} Eintraege", entries.len()
|
|
));
|
|
let by_id: HashMap<i64, &RemoteEntry> = entries.iter().map(|e| (e.id, e)).collect();
|
|
|
|
fn rel_path<'a>(
|
|
entry: &'a RemoteEntry,
|
|
by_id: &HashMap<i64, &'a RemoteEntry>,
|
|
) -> 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_FLAG_OPEN_REPARSE_POINT,
|
|
FILE_WRITE_ATTRIBUTES, FILE_READ_ATTRIBUTES,
|
|
FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE, OPEN_EXISTING,
|
|
};
|
|
|
|
let path_wide = U16CString::from_str(file.to_string_lossy().as_ref())
|
|
.map_err(|e| e.to_string())?;
|
|
// CfSetPinState / CfDehydratePlaceholder brauchen WRITE_ATTRIBUTES.
|
|
// OPEN_REPARSE_POINT verhindert, dass das Oeffnen selbst eine
|
|
// Hydration ausloest (sonst waere Unpin bedeutungslos).
|
|
let handle = unsafe {
|
|
CreateFileW(
|
|
PCWSTR(path_wide.as_ptr()),
|
|
(FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES).0,
|
|
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
|
None,
|
|
OPEN_EXISTING,
|
|
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
|
|
None,
|
|
)
|
|
}
|
|
.map_err(|e| format!("open: {e}"))?;
|
|
|
|
let state = if pinned {
|
|
CF::CF_PIN_STATE_PINNED
|
|
} else {
|
|
CF::CF_PIN_STATE_UNPINNED
|
|
};
|
|
let set_res = unsafe {
|
|
CF::CfSetPinState(handle, state, CF::CF_SET_PIN_FLAG_NONE, None)
|
|
};
|
|
|
|
// "Speicher freigeben" (unpin): sofort dehydrieren, damit der Platz
|
|
// wirklich frei wird. CfSetPinState allein aendert nur den Zustand.
|
|
// Der Rueckgabewert wird geloggt aber nicht hart als Fehler gewertet,
|
|
// weil die Datei evtl. schon dehydriert ist.
|
|
let dehydrate_err = if !pinned && set_res.is_ok() {
|
|
let r = unsafe {
|
|
CF::CfDehydratePlaceholder(
|
|
handle,
|
|
0,
|
|
-1,
|
|
CF::CF_DEHYDRATE_FLAG_NONE,
|
|
None,
|
|
)
|
|
};
|
|
r.err().map(|e| format!("{:?}", e))
|
|
} else {
|
|
None
|
|
};
|
|
|
|
unsafe {
|
|
let _ = windows::Win32::Foundation::CloseHandle(handle);
|
|
}
|
|
|
|
// Log-Verzeichnis ist der Mount-Ordner oder dessen Parent
|
|
let log_dir = file
|
|
.ancestors()
|
|
.find(|p| p.parent().is_some())
|
|
.map(|p| p.to_path_buf())
|
|
.unwrap_or_else(|| file.to_path_buf());
|
|
log_msg(
|
|
&log_dir,
|
|
&format!(
|
|
"set_pin_state file={} pinned={} result={:?} dehydrate_err={:?}",
|
|
file.display(),
|
|
pinned,
|
|
set_res,
|
|
dehydrate_err
|
|
),
|
|
);
|
|
|
|
set_res.map_err(|e| format!("CfSetPinState: {e}"))?;
|
|
Ok(())
|
|
}
|