fix(cloud-files): Pin/Unpin tatsaechlich wirksam machen + CLI-Logging

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>
This commit is contained in:
Stefan Hacker 2026-04-23 17:29:25 +02:00
parent 85dae4377f
commit 3c340f9653
2 changed files with 79 additions and 12 deletions

View File

@ -509,20 +509,24 @@ fn create_placeholder(
pub fn set_pin_state(file: &Path, pinned: bool) -> Result<(), String> { pub fn set_pin_state(file: &Path, pinned: bool) -> Result<(), String> {
use windows::Win32::Storage::FileSystem::{ use windows::Win32::Storage::FileSystem::{
CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_READ_ATTRIBUTES, CreateFileW, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING, 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()) let path_wide = U16CString::from_str(file.to_string_lossy().as_ref())
.map_err(|e| e.to_string())?; .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 { let handle = unsafe {
CreateFileW( CreateFileW(
PCWSTR(path_wide.as_ptr()), PCWSTR(path_wide.as_ptr()),
FILE_READ_ATTRIBUTES.0, (FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES).0,
FILE_SHARE_READ | FILE_SHARE_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
None, None,
OPEN_EXISTING, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
None, None,
) )
} }
@ -533,12 +537,50 @@ pub fn set_pin_state(file: &Path, pinned: bool) -> Result<(), String> {
} else { } else {
CF::CF_PIN_STATE_UNPINNED CF::CF_PIN_STATE_UNPINNED
}; };
let res = unsafe { let set_res = unsafe {
CF::CfSetPinState(handle, state, CF::CF_SET_PIN_FLAG_NONE, None) 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 { unsafe {
let _ = windows::Win32::Foundation::CloseHandle(handle); let _ = windows::Win32::Foundation::CloseHandle(handle);
} }
res.map_err(|e| format!("CfSetPinState: {e}"))?;
// 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(()) Ok(())
} }

View File

@ -1102,24 +1102,49 @@ async fn fetch_remote_entries(
/// Short-circuit fuer Shell-Kontextmenue-Aufrufe: /// Short-circuit fuer Shell-Kontextmenue-Aufrufe:
/// `minicloud-sync --pin <file>` oder `--unpin <file>` fuehrt die /// `minicloud-sync --pin <file>` oder `--unpin <file>` fuehrt die
/// Aktion direkt aus und beendet. Kein UI, kein Tray. /// Aktion direkt aus und beendet. Kein UI, kein Tray.
/// Logs landen in %LOCALAPPDATA%\MiniCloud Sync\cli.log - sonst
/// wuerden wir vom Explorer gestartete Prozesse nie debuggen koennen.
#[cfg(windows)] #[cfg(windows)]
fn handle_cli_shortcuts() { fn handle_cli_shortcuts() {
use std::io::Write;
let args: Vec<String> = std::env::args().collect(); let args: Vec<String> = std::env::args().collect();
if args.len() < 3 { if args.len() < 3 {
return; return;
} }
let cmd = args[1].as_str(); let cmd = args[1].as_str();
if cmd != "--pin" && cmd != "--unpin" {
return;
}
let path = std::path::PathBuf::from(&args[2]); let path = std::path::PathBuf::from(&args[2]);
let log_path = dirs::data_local_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join("MiniCloud Sync")
.join("cli.log");
if let Some(p) = log_path.parent() {
let _ = std::fs::create_dir_all(p);
}
let log = |msg: &str| {
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
{
let _ = writeln!(f, "[{}] {}", chrono::Utc::now().to_rfc3339(), msg);
}
};
log(&format!("CLI invoked: {} {}", cmd, path.display()));
let result = match cmd { let result = match cmd {
"--pin" => cloud_files::pin_file(&path), "--pin" => cloud_files::pin_file(&path),
"--unpin" => cloud_files::unpin_file(&path), "--unpin" => cloud_files::unpin_file(&path),
_ => return, _ => unreachable!(),
}; };
if let Err(e) = result { match &result {
eprintln!("[cloud_files CLI] {cmd} failed: {e}"); Ok(()) => log(&format!("{cmd} OK: {}", path.display())),
std::process::exit(1); Err(e) => log(&format!("{cmd} FAILED: {e}")),
} }
std::process::exit(0); std::process::exit(if result.is_ok() { 0 } else { 1 });
} }
#[cfg(not(windows))] #[cfg(not(windows))]