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
@@ -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));+ } }