Skip to content

Commit 8d63132

Browse files
authored
Batch 4: Download subsystem integration — replace old DownloadManager (#368)
- Create DownloadPlanBuilder: cross-version selection, version chain, frozen package filtering, MinClientVersion compatibility check - Create HttpDownloadSource: bridges VersionService → IDownloadSource, validates both client and upgrade versions in one call - Refactor DefaultDownloadOrchestrator: accept DownloadPlan instead of raw URLs, integrate SHA256 verification via DefaultDownloadPipeline - Update IDownloadOrchestrator interface to accept DownloadPlan - Refactor ClientUpdateStrategy: replace old DownloadManager/DownloadTask with HttpDownloadSource + DownloadPlanBuilder + DefaultDownloadOrchestrator - Mark DownloadManager as [Obsolete] — still used by OSS and Silent (to be removed in later batches) Closes #367
1 parent 6fd59f4 commit 8d63132

6 files changed

Lines changed: 315 additions & 81 deletions

File tree

src/c#/GeneralUpdate.Core/Download/Abstractions/IDownloadOrchestrator.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@ namespace GeneralUpdate.Core.Download.Abstractions;
99
/// <summary>Orchestrates batch downloads with concurrency control.</summary>
1010
public interface IDownloadOrchestrator
1111
{
12+
/// <summary>
13+
/// Execute downloads for all assets in the plan.
14+
/// Handles parallelism, retry, and SHA256 verification.
15+
/// </summary>
1216
Task<DownloadReport> ExecuteAsync(
13-
IReadOnlyList<string> urls,
17+
DownloadPlan plan,
1418
string destDir,
1519
int maxConcurrency = 3,
1620
IProgress<DownloadProgress>? progress = null,

src/c#/GeneralUpdate.Core/Download/DownloadManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
namespace GeneralUpdate.Core.Download
99
{
10+
[Obsolete("Use IDownloadOrchestrator + DefaultDownloadOrchestrator instead. Will be removed in v11.")]
1011
public class DownloadManager(string path, string format, int timeOut)
1112
{
1213
#region Private Members
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using GeneralUpdate.Core.Download.Models;
5+
6+
namespace GeneralUpdate.Core.Download;
7+
8+
/// <summary>
9+
/// Builds a DownloadPlan from download assets.
10+
/// Handles cross-version package selection, version chain building,
11+
/// frozen package filtering, and forced update marking.
12+
/// </summary>
13+
public static class DownloadPlanBuilder
14+
{
15+
/// <summary>
16+
/// Build a download plan from a list of download assets.
17+
/// </summary>
18+
/// <param name="assets">Assets from the download source.</param>
19+
/// <param name="currentVersion">Current client version string.</param>
20+
/// <returns>A DownloadPlan with ordered assets, or DownloadPlan.Empty if no update is needed.</returns>
21+
public static DownloadPlan Build(IEnumerable<DownloadAsset> assets, string currentVersion)
22+
{
23+
if (assets == null) return DownloadPlan.Empty;
24+
25+
// 1. Filter out frozen packages
26+
var active = assets
27+
.Where(a => !a.IsFreeze)
28+
.ToList();
29+
30+
if (active.Count == 0) return DownloadPlan.Empty;
31+
32+
// 2. Check for forced update
33+
var isForcibly = active.Any(a => a.IsForcibly);
34+
35+
// 3. Look for a cross-version package that matches our current version
36+
var crossVersion = active
37+
.Where(a => a.IsCrossVersion
38+
&& !string.IsNullOrEmpty(a.FromVersion)
39+
&& VersionEquals(a.FromVersion!, currentVersion))
40+
.OrderByDescending(a => ParseVersion(a.Version))
41+
.FirstOrDefault();
42+
43+
if (crossVersion != null)
44+
{
45+
// Single download — jump directly to target version
46+
return new DownloadPlan(new[] { crossVersion }, isForcibly);
47+
}
48+
49+
// 4. Build version chain from non-cross-version packages
50+
var chain = BuildVersionChain(active.Where(a => !a.IsCrossVersion), currentVersion);
51+
if (chain.Count == 0) return DownloadPlan.Empty;
52+
53+
return new DownloadPlan(chain, isForcibly);
54+
}
55+
56+
/// <summary>
57+
/// Build a version chain: keep versions higher than current,
58+
/// check MinClientVersion compatibility.
59+
/// </summary>
60+
private static List<DownloadAsset> BuildVersionChain(IEnumerable<DownloadAsset> assets, string currentVersion)
61+
{
62+
var current = ParseVersion(currentVersion);
63+
64+
return assets
65+
.Where(a =>
66+
{
67+
var pv = ParseVersion(a.Version);
68+
if (pv == null) return false;
69+
return pv > current;
70+
})
71+
.Where(a => IsCompatible(a.MinClientVersion, currentVersion))
72+
.OrderBy(a => ParseVersion(a.Version))
73+
.ToList();
74+
}
75+
76+
/// <summary>
77+
/// Check if MinClientVersion is compatible with the current version.
78+
/// A package with MinClientVersion higher than current is not applicable.
79+
/// </summary>
80+
private static bool IsCompatible(string? minClientVersion, string currentVersion)
81+
{
82+
if (string.IsNullOrEmpty(minClientVersion)) return true;
83+
var min = ParseVersion(minClientVersion);
84+
var cur = ParseVersion(currentVersion);
85+
if (min == null || cur == null) return true;
86+
return cur >= min;
87+
}
88+
89+
/// <summary>Parse a version string, returning null on failure.</summary>
90+
private static Version? ParseVersion(string? version)
91+
{
92+
if (string.IsNullOrWhiteSpace(version)) return null;
93+
return Version.TryParse(version, out var v) ? v : null;
94+
}
95+
96+
/// <summary>Compare two version strings for equality.</summary>
97+
private static bool VersionEquals(string a, string b)
98+
{
99+
var va = ParseVersion(a);
100+
var vb = ParseVersion(b);
101+
return va != null && vb != null && va == vb;
102+
}
103+
}

src/c#/GeneralUpdate.Core/Download/Orchestrators/DefaultDownloadOrchestrator.cs

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
using GeneralUpdate.Core.Download.Executors;
1111
using GeneralUpdate.Core.Download.Policy;
1212
using GeneralUpdate.Core.Download.Models;
13+
using GeneralUpdate.Core.Download.Pipeline;
1314

1415
namespace GeneralUpdate.Core.Download.Orchestrators;
1516

1617
/// <summary>
17-
/// Default download orchestrator with parallel execution and concurrency limit.
18+
/// Default download orchestrator with parallel execution, concurrency limit,
19+
/// SHA256 verification, and progress reporting.
1820
/// </summary>
1921
public class DefaultDownloadOrchestrator : IDownloadOrchestrator
2022
{
@@ -27,36 +29,63 @@ public DefaultDownloadOrchestrator(HttpClient httpClient, IDownloadPolicy? polic
2729
_policy = policy ?? new DefaultRetryPolicy();
2830
}
2931

32+
/// <summary>Execute downloads for all assets in the plan.</summary>
3033
public async Task<DownloadReport> ExecuteAsync(
31-
IReadOnlyList<string> urls,
34+
DownloadPlan plan,
3235
string destDir,
3336
int maxConcurrency = 3,
3437
IProgress<DownloadProgress>? progress = null,
3538
CancellationToken token = default)
3639
{
40+
if (plan == null || !plan.HasAssets)
41+
return new DownloadReport(Array.Empty<DownloadResult>(), 0, TimeSpan.Zero, 0, 0);
42+
3743
var sw = Stopwatch.StartNew();
3844
var results = new List<DownloadResult>();
3945
var sem = new SemaphoreSlim(maxConcurrency);
4046
long totalBytes = 0;
4147

42-
var tasks = urls.Select(async (url, i) =>
48+
var tasks = plan.Assets.Select(async asset =>
4349
{
4450
await sem.WaitAsync(token).ConfigureAwait(false);
4551
try
4652
{
47-
var fileName = Path.GetFileName(new Uri(url).AbsolutePath);
48-
if (string.IsNullOrEmpty(fileName)) fileName = $"download_{i}";
53+
var fileName = GetFileName(asset);
4954
var destPath = Path.Combine(destDir, fileName);
5055

5156
var executor = new HttpDownloadExecutor(_httpClient);
52-
var r = await _policy.ExecuteAsync(ct =>
53-
executor.ExecuteAsync(url, destPath, progress, ct), token)
54-
.ConfigureAwait(false);
57+
var pipeline = new DefaultDownloadPipeline(asset.SHA256);
58+
59+
var result = await _policy.ExecuteAsync(async ct =>
60+
{
61+
// Download
62+
var downloadResult = await executor.ExecuteAsync(
63+
asset.Url, destPath,
64+
progress != null ? new AssetProgressReporter(progress, asset.Name) : null,
65+
ct).ConfigureAwait(false);
66+
67+
if (!downloadResult.Success)
68+
return downloadResult;
69+
70+
// Verify (SHA256)
71+
try
72+
{
73+
await pipeline.ProcessAsync(destPath, ct).ConfigureAwait(false);
74+
}
75+
catch (Exception ex)
76+
{
77+
return new DownloadResult(asset.Url, destPath,
78+
downloadResult.DownloadedBytes, downloadResult.Duration,
79+
downloadResult.RetryCount, false, $"SHA256 verification failed: {ex.Message}");
80+
}
81+
82+
return downloadResult;
83+
}, token).ConfigureAwait(false);
5584

5685
lock (results)
5786
{
58-
results.Add(r);
59-
if (r.Success) totalBytes += r.DownloadedBytes;
87+
results.Add(result);
88+
if (result.Success) totalBytes += result.DownloadedBytes;
6089
}
6190
}
6291
finally { sem.Release(); }
@@ -72,4 +101,28 @@ public async Task<DownloadReport> ExecuteAsync(
72101
results.Count(r => r.Success),
73102
results.Count(r => !r.Success));
74103
}
104+
105+
private static string GetFileName(DownloadAsset asset)
106+
{
107+
try
108+
{
109+
var name = Path.GetFileName(new Uri(asset.Url).AbsolutePath);
110+
if (!string.IsNullOrEmpty(name)) return name;
111+
}
112+
catch { }
113+
return $"{asset.Name}.{asset.Version}";
114+
}
115+
116+
/// <summary>Wraps progress reporting to include the asset name.</summary>
117+
private sealed class AssetProgressReporter : IProgress<DownloadProgress>
118+
{
119+
private readonly IProgress<DownloadProgress> _inner;
120+
private readonly string _assetName;
121+
public AssetProgressReporter(IProgress<DownloadProgress> inner, string assetName)
122+
{ _inner = inner; _assetName = assetName; }
123+
public void Report(DownloadProgress value)
124+
{
125+
_inner.Report(value with { AssetName = _assetName });
126+
}
127+
}
75128
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using GeneralUpdate.Core.Configuration;
6+
using GeneralUpdate.Core.Download.Models;
7+
using GeneralUpdate.Core.Network;
8+
9+
namespace GeneralUpdate.Core.Download.Sources;
10+
11+
/// <summary>
12+
/// HTTP download source — calls the version validation API
13+
/// and converts the server response to a list of DownloadAssets.
14+
/// </summary>
15+
public class HttpDownloadSource : Abstractions.IDownloadSource
16+
{
17+
private readonly string _updateUrl;
18+
private readonly string _clientVersion;
19+
private readonly string? _upgradeClientVersion;
20+
private readonly string _appSecretKey;
21+
private readonly int _platform;
22+
private readonly string? _productId;
23+
private readonly string? _scheme;
24+
private readonly string? _token;
25+
26+
public HttpDownloadSource(
27+
string updateUrl,
28+
string clientVersion,
29+
string? upgradeClientVersion,
30+
string appSecretKey,
31+
int platform,
32+
string? productId,
33+
string? scheme,
34+
string? token)
35+
{
36+
_updateUrl = updateUrl;
37+
_clientVersion = clientVersion;
38+
_upgradeClientVersion = upgradeClientVersion;
39+
_appSecretKey = appSecretKey;
40+
_platform = platform;
41+
_productId = productId;
42+
_scheme = scheme;
43+
_token = token;
44+
}
45+
46+
/// <summary>Call version API and return download assets.</summary>
47+
public async Task<IReadOnlyList<DownloadAsset>> ListAsync(CancellationToken token = default)
48+
{
49+
var mainResp = await VersionService.Validate(
50+
_updateUrl, _clientVersion, AppType.ClientApp,
51+
_appSecretKey, _platform, _productId,
52+
_scheme, _token);
53+
54+
var upgradeResp = await VersionService.Validate(
55+
_updateUrl, _upgradeClientVersion ?? _clientVersion, AppType.UpgradeApp,
56+
_appSecretKey, _platform, _productId,
57+
_scheme, _token);
58+
59+
var assets = new List<DownloadAsset>();
60+
61+
if (mainResp?.Body != null)
62+
{
63+
foreach (var v in mainResp.Body)
64+
assets.Add(MapVersionInfo(v));
65+
}
66+
67+
if (upgradeResp?.Body != null)
68+
{
69+
foreach (var v in upgradeResp.Body)
70+
assets.Add(MapVersionInfo(v));
71+
}
72+
73+
return assets;
74+
}
75+
76+
private static DownloadAsset MapVersionInfo(VersionInfo v)
77+
{
78+
return new DownloadAsset(
79+
Name: v.Name ?? v.Version ?? "unknown",
80+
Url: v.Url ?? string.Empty,
81+
Size: v.Size ?? 0,
82+
SHA256: v.Hash,
83+
Version: v.Version ?? "0.0.0",
84+
IsForcibly: v.IsForcibly == true
85+
);
86+
}
87+
}

0 commit comments

Comments
 (0)