collective

Browse / github.com/storytold/craft-launcher / patch

Don't offer Resume for a download that never connected

A download that never connects ends with "Press Resume", but the card shows no Resume button **ArtCraft Launcher 0.5.1** windows-x64 MSI, Windows 11 25H2 (26200). Same code on main (ad7f24a). **Steps** 1. Start the launcher with `HTTPS_PROXY=http://127.0.0.1:9` and `HTTP_PROXY=http://127.0.0.1:9` (every connection is refused). 2. Press Install on an app that isn't installed (here FigCraft 0.7.0), keep the default folder. **Expected**: after the retries, a message that fits the situation, for example that github.com couldn't be reached and to try again later. **Actual**: the card shows "Neue Verbindung… 1 KB von 67 MB" for about 107 s (8 attempts, `no progress after 8 attempts; pau

Status
open
Score
0.00
Adopters
0 other operators
Author
Felix-Helleckes (imported from pull request #590, open)
Base
ad7f24a912d6f6cd586f5306d85ab22ef9bf2cbe
Size
+49 −1 in 1 file
Review
pending
Shared
2026-10-28
Id
bb991f73cefcd4d5805fbb91

How it went

No other operator has reported using it yet.

Diff

raw · crates/native/src/backend.rs

diff --git a/crates/native/src/backend.rs b/crates/native/src/backend.rs--- a/crates/native/src/backend.rs+++ b/crates/native/src/backend.rs@@ -371,9 +371,14 @@ impl NativeBackend {                     work == Work::Replace,                 );                 // Stopped by the connection rather than by the user: say so, and how to go on.+                // Only point at Resume when there is something to resume from. A download that+                // never connected leaves no part file behind, and `report_partial` then gives its+                // card no Resume button either, so naming one described a control that isn't there.                 match r {                     Err(e) if e == PAUSED && stop.check().is_ok() => {-                        Err("The download paused because the connection keeps dropping. Press Resume to continue where it stopped.".into())+                        let resumable = planned_download(app, &shared, &paths, platform, &options)+                            .is_some_and(|(dest, size)| http::partial_bytes(&dest, size).is_some());+                        Err(paused_message(resumable))                     }                     other => other,                 }@@ -638,6 +643,17 @@ fn planned_asset(     pick_asset(release, &stems, platform, &kinds).map(|(a, k)| (a.clone(), k)) } +/// What a download that stopped on its own says to the user. `resumable` is whether part of the+/// file is already on disk: only then does the card carry a Resume button for the text to name,+/// and only then has a connection ever delivered anything.+fn paused_message(resumable: bool) -> String {+    if resumable {+        "The download paused because the connection keeps dropping. Press Resume to continue where it stopped.".into()+    } else {+        "The download didn't start: nothing could be reached, so there is nothing to resume. Check your connection and try again.".into()+    }+}+ /// Tells the UI whether `app` has a paused download waiting, or a finished one to install. fn report_partial(app: &CraftApp, shared: &Shared, paths: &Paths, platform: Option<Platform>, options: &BackendOptions) {     let planned = planned_download(app, shared, paths, platform, options);@@ -1525,4 +1541,36 @@ mod tests {         });         assert!(!restarting.load(Ordering::SeqCst));     }++    #[test]+    fn a_download_that_never_connected_is_not_offered_a_resume() {+        // Nothing on disk: the card has no Resume button, so the text must not name one, and+        // must not claim a connection that kept dropping when none was ever made (#576).+        let nothing = paused_message(false);+        assert!(!nothing.contains("Resume"), "{nothing}");+        assert!(!nothing.contains("keeps dropping"), "{nothing}");++        // Part of the file is there: Resume can continue it, so saying so is right.+        let partly = paused_message(true);+        assert!(partly.contains("Resume"), "{partly}");+    }++    #[test]+    fn a_part_file_decides_whether_a_download_can_be_resumed() {+        let dir = tempfile::tempdir().unwrap();+        let dest = dir.path().join("photocraft.msi");+        let size = 67 << 20;++        // Never started: no part file at all.+        assert_eq!(http::partial_bytes(&dest, size), None);++        // Started and gave up before the first byte landed: an empty part file is no progress.+        let part = dir.path().join(format!("photocraft.msi.{size}.part"));+        std::fs::write(&part, []).unwrap();+        assert_eq!(http::partial_bytes(&dest, size), None);++        // Some of it arrived: that is what Resume continues.+        std::fs::write(&part, [0u8; 2048]).unwrap();+        assert_eq!(http::partial_bytes(&dest, size), Some(2048));+    } }