fix(bootstrap): resume downloads without discarding a valid archive

Resuming a partial download leaned entirely on the remote size from a
HEAD request. When that size could not be determined the code played it
safe by deleting the partial file and starting over, so a flaky HEAD (or
an offline mirror) threw away gigabytes that were fine to resume. It also
could not tell a real size from an error page, and if the server answered
a ranged request with the whole file (200, not 206) libcurl could append
it to the partial and silently corrupt the archive.

- The HEAD now returns transport status, HTTP code, and size together,
  and a size counts as authoritative only on a 2xx reply with a positive
  Content-Length.
- A pure helper decides what to do from the existing bytes and that
  authoritative flag: start fresh, truncate a stale oversized file, reuse
  a complete one, or resume with a ranged GET. When the size is not
  authoritative the partial is kept and a ranged GET is attempted rather
  than discarded.
- A ranged request that comes back as 200 instead of 206 now restarts
  cleanly from scratch instead of appending to the partial.
- The free-space pre-check runs only when the size is authoritative, and
  a failed download reports how many bytes were kept for the retry.

Tests cover the resume-decision helper across the fresh, stale, complete,
resuming, and non-authoritative cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 20:57:21 -05:00
co-authored by Claude Opus 4.8
parent 4d9d528566
commit 667c89ba51
3 changed files with 83 additions and 40 deletions
+7 -3
View File
@@ -141,6 +141,10 @@ public:
/// expectedHexLen hex chars; "" otherwise (an HTTP error page is never a checksum).
static std::string parseChecksumFile(const std::string& content, std::size_t expectedHexLen);
enum class ResumeAction { Fresh, TruncateAndFresh, ReuseComplete, RangedGet };
/// Pure, no-I/O. authoritative = HEAD was 2xx with a positive Content-Length.
static ResumeAction decideResume(long long existingBytes, bool authoritative, long long remoteSize);
private:
mutable std::mutex mutex_;
Progress progress_;
@@ -173,9 +177,9 @@ private:
/// Verify downloaded zip against remote checksums. Returns true if valid.
bool verifyChecksums(const std::string& zipPath, const std::string& baseUrl);
/// Perform a HEAD request to get remote file size.
/// Returns -1 on failure.
long long getRemoteFileSize(const std::string& url);
struct RemoteInfo { bool transportOk = false; long httpCode = 0; long long size = -1; };
/// HEAD the remote file. transportOk=false on any curl error; size=-1 when absent.
RemoteInfo getRemoteFileSize(const std::string& url);
/// Byte offset of the partial file when resuming a download.
/// Added to dlnow in progress reports so the bar reflects true total.