From 627456721980c70845eeb06c53abe4a39a996916 Mon Sep 17 00:00:00 2001 From: Stefan Hacker Date: Wed, 15 Apr 2026 09:24:51 +0200 Subject: [PATCH] fix(cloud-files): Timeout-Ursachen im FETCH_DATA-Callback beheben - HTTP-Client bekommt 60s-Timeout (statt unendlich) - Bei Send-/Netzwerkfehler wird CfExecute immer mit Failure-Status abgeschlossen, damit Explorer nicht ins OS-Timeout laeuft - Wenn Server kein Range unterstuetzt (200 statt 206), wird aus dem Full-Body der angeforderte Bereich herausgeschnitten und die tatsaechliche Laenge an CfExecute uebergeben - Fehler werden in \.minicloud-cloudfiles.log geschrieben, damit man das Problem bei Timeout ueberhaupt sehen kann Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src-tauri/src/cloud_files/windows.rs | 55 ++++++++++++++----- 1 file changed, 40 insertions(+), 15 deletions(-) diff --git a/clients/desktop/src-tauri/src/cloud_files/windows.rs b/clients/desktop/src-tauri/src/cloud_files/windows.rs index 91b5e42..ad6458e 100644 --- a/clients/desktop/src-tauri/src/cloud_files/windows.rs +++ b/clients/desktop/src-tauri/src/cloud_files/windows.rs @@ -171,19 +171,40 @@ unsafe extern "system" fn on_fetch_data( // HTTPS-Download im separaten Thread (Callback darf nicht blockieren). let ctx = ctx_snapshot(); std::thread::spawn(move || { - let _ = transfer_range(connection_key, transfer_key, file_id, offset, length, ctx); + if let Err(e) = transfer_range(connection_key, transfer_key, file_id, offset, length, &ctx) + { + 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); + } }); } +fn log_err(mount: &Path, msg: &str) { + use std::io::Write; + let log = mount.join(".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 transfer_range( connection_key: CF::CF_CONNECTION_KEY, transfer_key: i64, file_id: i64, offset: i64, length: u64, - ctx: CloudContext, + ctx: &CloudContext, ) -> Result<(), String> { - let client = reqwest::blocking::Client::new(); + 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('/'), @@ -195,22 +216,26 @@ fn transfer_range( .bearer_auth(&ctx.access_token) .header("Range", &range) .send() - .map_err(|e| format!("download: {e}"))?; + .map_err(|e| format!("send: {e}"))?; 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 {}", status)); } - let bytes = resp - .bytes() - .map_err(|e: reqwest::Error| 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())?; + // 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(