v0.3: resumable downloads with HTTP Range, Polly retry, persistent state
This is the robustness layer needed for real 14 GB builds. A 99%-complete
download that gets interrupted no longer means re-downloading 14 GB.
DownloadManager
---------------
- Range: bytes={resumeFrom}- on every (re)attempt; reads resumeFrom from
the actual size of the .partial file so retries always resume from real
on-disk state, not from a remembered counter.
- If-Range: ETag (or Last-Modified fallback). When the server returns 200
instead of 206 we know the resource changed under us, so we discard
.partial and start fresh.
- Polly resilience pipeline: 6 retries, exponential 1-32s with jitter,
on HttpRequestException / IOException / TimeoutException / 5xx / 408 /
429. Each retry re-evaluates resumeFrom from disk, so the server is
asked only for what's actually missing.
- state.json persisted every 5s OR every 100 MiB, whichever comes first,
via atomic write-then-rename. Holds url, total, downloaded, sha256,
etag, last-modified, and the .partial path.
- Disk-space check happens once at fresh-start (1.05x expected size); a
resume doesn't redo it.
- On final success: SHA-256 of the assembled .partial verified, then
atomic rename to .zip and state.json deleted.
DownloadStateStore
------------------
- New IDownloadStateStore in PSLauncher.Core/Downloads.
- Stores under %LocalAppData%/PSLauncher/downloads/.
- Save / Load / Discard / ScanResumable. Tolerates malformed state files
by ignoring them.
UI hint
-------
VersionRowViewModel now has ResumableBytes; when > 0, the install button
label switches to "↻ Reprendre (X%)" computed from
ResumableBytes / Remote.Download.SizeBytes. MainViewModel.RebuildList
queries IDownloadManager.GetResumableState(version) for each remote-only
row and populates ResumableBytes. Both the featured hero card and the
compact rows bind to InstallButtonLabel.
API change
----------
IDownloadManager gains GetResumableState(version) and
DiscardResumableState(version) so callers (and the UI) can reason about
in-progress downloads without poking at the filesystem directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,24 @@ public sealed partial class VersionRowViewModel : ObservableObject
|
||||
[ObservableProperty] private double _progressPercent;
|
||||
[ObservableProperty] private string? _progressDetail;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(InstallButtonLabel))]
|
||||
[NotifyPropertyChangedFor(nameof(HasResumableDownload))]
|
||||
private long _resumableBytes;
|
||||
|
||||
public bool HasResumableDownload => ResumableBytes > 0;
|
||||
|
||||
public string InstallButtonLabel
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!HasResumableDownload) return "⬇ Installer";
|
||||
if (Remote is null || Remote.Download.SizeBytes <= 0) return "↻ Reprendre";
|
||||
var pct = (double)ResumableBytes / Remote.Download.SizeBytes * 100.0;
|
||||
return $"↻ Reprendre ({pct:0}%)";
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsInstalled => State is VersionRowState.InstalledIdle or VersionRowState.Uninstalling;
|
||||
public bool IsRemoteOnly => State is VersionRowState.AvailableIdle;
|
||||
public bool IsBusy => State is VersionRowState.Downloading or VersionRowState.Installing or VersionRowState.Uninstalling;
|
||||
|
||||
Reference in New Issue
Block a user