feat(releases): add SelfUpdater.DownloadAndVerifyAsync
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
@@ -92,4 +93,34 @@ public static partial class SelfUpdater
|
|||||||
File.Copy(currentExePath, oldPath, overwrite: false);
|
File.Copy(currentExePath, oldPath, overwrite: false);
|
||||||
return launchProcess(oldPath);
|
return launchProcess(oldPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async Task<string?> DownloadAndVerifyAsync(
|
||||||
|
IReleaseClient releases,
|
||||||
|
ReleaseAsset installerAsset,
|
||||||
|
ReleaseAsset checksumsAsset,
|
||||||
|
string tempDir,
|
||||||
|
IProgress<long> progress,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
var installerPath = Path.Combine(tempDir, installerAsset.Name);
|
||||||
|
var checksumsPath = Path.Combine(tempDir, "checksums.txt");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await releases.DownloadAsync(installerAsset.BrowserDownloadUrl, installerPath, progress, ct);
|
||||||
|
await releases.DownloadAsync(checksumsAsset.BrowserDownloadUrl, checksumsPath, new Progress<long>(_ => { }), ct);
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is HttpRequestException or IOException or TaskCanceledException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var checksumsText = await File.ReadAllTextAsync(checksumsPath, ct);
|
||||||
|
var map = ChecksumVerifier.ParseChecksumsFile(checksumsText);
|
||||||
|
if (!map.TryGetValue(installerAsset.Name, out var expected))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return ChecksumVerifier.Verify(installerPath, expected) ? installerPath : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,3 +177,80 @@ public class SelfUpdaterReplaceSelfTests : IDisposable
|
|||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class SelfUpdaterDownloadTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _tempDir;
|
||||||
|
|
||||||
|
public SelfUpdaterDownloadTests()
|
||||||
|
{
|
||||||
|
_tempDir = Path.Combine(Path.GetTempPath(), "ClaudeDo.Releases.Tests-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(_tempDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() { try { Directory.Delete(_tempDir, true); } catch { } }
|
||||||
|
|
||||||
|
private sealed class StubReleaseClient : IReleaseClient
|
||||||
|
{
|
||||||
|
public string FileContent { get; set; } = "";
|
||||||
|
public string ChecksumsBody { get; set; } = "";
|
||||||
|
|
||||||
|
public Task<GiteaRelease?> GetLatestReleaseAsync(CancellationToken ct) => Task.FromResult<GiteaRelease?>(null);
|
||||||
|
|
||||||
|
public async Task DownloadAsync(string url, string destPath, IProgress<long> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (url.EndsWith("checksums.txt", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(destPath, ChecksumsBody, ct);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await File.WriteAllTextAsync(destPath, FileContent, ct);
|
||||||
|
}
|
||||||
|
progress.Report(FileContent.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_MatchingChecksum_ReturnsPath()
|
||||||
|
{
|
||||||
|
var content = "FAKE-INSTALLER-BINARY";
|
||||||
|
var hash = Sha256Hex(content);
|
||||||
|
var client = new StubReleaseClient
|
||||||
|
{
|
||||||
|
FileContent = content,
|
||||||
|
ChecksumsBody = $"{hash} ClaudeDo.Installer-0.3.0.exe\n",
|
||||||
|
};
|
||||||
|
var installer = new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst", content.Length);
|
||||||
|
var checksums = new ReleaseAsset("checksums.txt", "https://x/checksums.txt", 100);
|
||||||
|
|
||||||
|
var path = await SelfUpdater.DownloadAndVerifyAsync(
|
||||||
|
client, installer, checksums, _tempDir, new Progress<long>(_ => { }), CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.NotNull(path);
|
||||||
|
Assert.Equal(content, await File.ReadAllTextAsync(path!));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Download_ChecksumMismatch_ReturnsNull()
|
||||||
|
{
|
||||||
|
var client = new StubReleaseClient
|
||||||
|
{
|
||||||
|
FileContent = "real",
|
||||||
|
ChecksumsBody = "deadbeef" + new string('0', 56) + " ClaudeDo.Installer-0.3.0.exe\n",
|
||||||
|
};
|
||||||
|
var installer = new ReleaseAsset("ClaudeDo.Installer-0.3.0.exe", "https://x/inst", 4);
|
||||||
|
var checksums = new ReleaseAsset("checksums.txt", "https://x/checksums.txt", 100);
|
||||||
|
|
||||||
|
var path = await SelfUpdater.DownloadAndVerifyAsync(
|
||||||
|
client, installer, checksums, _tempDir, new Progress<long>(_ => { }), CancellationToken.None);
|
||||||
|
|
||||||
|
Assert.Null(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Sha256Hex(string s)
|
||||||
|
{
|
||||||
|
using var sha = System.Security.Cryptography.SHA256.Create();
|
||||||
|
return Convert.ToHexString(sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(s))).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user