115 lines
5.2 KiB
C#
115 lines
5.2 KiB
C#
using System.IO;
|
|
using System.IO.Compression;
|
|
using ClaudeDo.Installer.Core;
|
|
using ClaudeDo.Releases;
|
|
|
|
namespace ClaudeDo.Installer.Steps;
|
|
|
|
public sealed class DownloadAndExtractStep : IInstallStep
|
|
{
|
|
private readonly IReleaseClient _releases;
|
|
|
|
public DownloadAndExtractStep(IReleaseClient releases)
|
|
{
|
|
_releases = releases;
|
|
}
|
|
|
|
public string Name => "Download and Extract";
|
|
|
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(ctx.InstallDirectory))
|
|
return StepResult.Fail("Install directory is not set.");
|
|
|
|
progress.Report("Fetching latest release metadata...");
|
|
var release = await _releases.GetLatestReleaseAsync(ct);
|
|
if (release is null)
|
|
return StepResult.Fail("Could not reach the release server. Check your network connection and try again.");
|
|
|
|
var zipAsset = release.Assets.FirstOrDefault(a =>
|
|
a.Name.StartsWith("ClaudeDo-", StringComparison.OrdinalIgnoreCase) &&
|
|
a.Name.EndsWith("-win-x64.zip", StringComparison.OrdinalIgnoreCase));
|
|
var checksumAsset = release.Assets.FirstOrDefault(a =>
|
|
a.Name.Equals("checksums.txt", StringComparison.OrdinalIgnoreCase));
|
|
|
|
if (zipAsset is null)
|
|
return StepResult.Fail("Release zip asset not found in release metadata.");
|
|
if (checksumAsset is null)
|
|
return StepResult.Fail("checksums.txt not found in release metadata.");
|
|
|
|
var scratchDir = Path.Combine(Path.GetTempPath(), "ClaudeDo-install-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(scratchDir);
|
|
|
|
try
|
|
{
|
|
var zipPath = Path.Combine(scratchDir, zipAsset.Name);
|
|
var checksumPath = Path.Combine(scratchDir, "checksums.txt");
|
|
|
|
var totalMb = zipAsset.Size / (1024 * 1024);
|
|
progress.Report($"Downloading {zipAsset.Name} ({totalMb} MB)...");
|
|
long lastReportedMb = -1;
|
|
await _releases.DownloadAsync(zipAsset.BrowserDownloadUrl, zipPath,
|
|
new Progress<long>(b =>
|
|
{
|
|
var mb = b / (1024 * 1024);
|
|
if (mb == lastReportedMb) return;
|
|
lastReportedMb = mb;
|
|
// Leading "\r" tells the UI to overwrite the previous line instead of appending.
|
|
progress.Report($"\r {mb} / {totalMb} MB downloaded");
|
|
}),
|
|
ct);
|
|
|
|
progress.Report("Downloading checksums...");
|
|
await _releases.DownloadAsync(checksumAsset.BrowserDownloadUrl, checksumPath,
|
|
new Progress<long>(_ => { }), ct);
|
|
|
|
progress.Report("Verifying checksum...");
|
|
var map = ChecksumVerifier.ParseChecksumsFile(await File.ReadAllTextAsync(checksumPath, ct));
|
|
if (!map.TryGetValue(zipAsset.Name, out var expectedHash))
|
|
return StepResult.Fail($"No checksum entry for {zipAsset.Name} in checksums.txt.");
|
|
if (!ChecksumVerifier.Verify(zipPath, expectedHash))
|
|
return StepResult.Fail("Checksum mismatch — the downloaded zip may be corrupt or tampered with.");
|
|
|
|
// Only after verification do we touch the install directory.
|
|
progress.Report("Stashing previous app/worker binaries...");
|
|
var appDest = Path.Combine(ctx.InstallDirectory, "app");
|
|
var workerDest = Path.Combine(ctx.InstallDirectory, "worker");
|
|
var appBak = appDest + ".bak";
|
|
var workerBak = workerDest + ".bak";
|
|
|
|
if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
|
|
if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
|
|
if (Directory.Exists(appDest)) Directory.Move(appDest, appBak);
|
|
if (Directory.Exists(workerDest)) Directory.Move(workerDest, workerBak);
|
|
|
|
progress.Report("Extracting...");
|
|
Directory.CreateDirectory(ctx.InstallDirectory);
|
|
try
|
|
{
|
|
ZipFile.ExtractToDirectory(zipPath, ctx.InstallDirectory, overwriteFiles: true);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Roll back to previous binaries.
|
|
if (Directory.Exists(appDest)) Directory.Delete(appDest, recursive: true);
|
|
if (Directory.Exists(workerDest)) Directory.Delete(workerDest, recursive: true);
|
|
if (Directory.Exists(appBak)) Directory.Move(appBak, appDest);
|
|
if (Directory.Exists(workerBak)) Directory.Move(workerBak, workerDest);
|
|
return StepResult.Fail(
|
|
$"Extraction failed; previous binaries have been restored: {ex.Message}.");
|
|
}
|
|
|
|
// Success — drop stash.
|
|
if (Directory.Exists(appBak)) Directory.Delete(appBak, recursive: true);
|
|
if (Directory.Exists(workerBak)) Directory.Delete(workerBak, recursive: true);
|
|
|
|
ctx.InstalledVersion = release.TagName.TrimStart('v', 'V');
|
|
return StepResult.Ok();
|
|
}
|
|
finally
|
|
{
|
|
try { Directory.Delete(scratchDir, recursive: true); } catch { /* best effort */ }
|
|
}
|
|
}
|
|
}
|