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 <mount>\.minicloud-cloudfiles.log geschrieben,
  damit man das Problem bei Timeout ueberhaupt sehen kann

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Hacker 2026-04-15 09:24:51 +02:00
parent 204dbb6ab5
commit 6274567219
1 changed files with 40 additions and 15 deletions

View File

@ -171,19 +171,40 @@ unsafe extern "system" fn on_fetch_data(
// HTTPS-Download im separaten Thread (Callback darf nicht blockieren). // HTTPS-Download im separaten Thread (Callback darf nicht blockieren).
let ctx = ctx_snapshot(); let ctx = ctx_snapshot();
std::thread::spawn(move || { 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( fn transfer_range(
connection_key: CF::CF_CONNECTION_KEY, connection_key: CF::CF_CONNECTION_KEY,
transfer_key: i64, transfer_key: i64,
file_id: i64, file_id: i64,
offset: i64, offset: i64,
length: u64, length: u64,
ctx: CloudContext, ctx: &CloudContext,
) -> Result<(), String> { ) -> 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!( let url = format!(
"{}/api/files/{}/download", "{}/api/files/{}/download",
ctx.server_url.trim_end_matches('/'), ctx.server_url.trim_end_matches('/'),
@ -195,22 +216,26 @@ fn transfer_range(
.bearer_auth(&ctx.access_token) .bearer_auth(&ctx.access_token)
.header("Range", &range) .header("Range", &range)
.send() .send()
.map_err(|e| format!("download: {e}"))?; .map_err(|e| format!("send: {e}"))?;
let status = resp.status(); let status = resp.status();
if !status.is_success() && status.as_u16() != 206 { if !status.is_success() && status.as_u16() != 206 {
let _ = complete_transfer(connection_key, transfer_key, None, offset, length);
return Err(format!("HTTP {}", status)); return Err(format!("HTTP {}", status));
} }
let bytes = resp let bytes = resp.bytes().map_err(|e: reqwest::Error| e.to_string())?;
.bytes() // Wenn Server kein Range unterstuetzt und volle Datei liefert,
.map_err(|e: reqwest::Error| e.to_string())?; // aus dem Body den angeforderten Bereich ausschneiden.
complete_transfer( let slice: &[u8] = if status.as_u16() == 206 {
connection_key, &bytes[..]
transfer_key, } else {
Some(&bytes), let start = offset as usize;
offset, let end = (start + length as usize).min(bytes.len());
length, if start >= bytes.len() {
) &[]
} else {
&bytes[start..end]
}
};
complete_transfer(connection_key, transfer_key, Some(slice), offset, slice.len() as u64)
} }
fn complete_transfer( fn complete_transfer(