From 447bacfb723a2749ee2927fc4d6b7132d942dc97 Mon Sep 17 00:00:00 2001 From: Kevin Bost Date: Fri, 27 Dec 2024 00:20:50 -0800 Subject: [PATCH] Cleanup Flow client to only create API client onces This is per logical operation, not a real singleton. Fixed up issue with deltas always assuming there would be a successful response. --- src/vpk/Velopack.Flow/FlowApiExtensions.cs | 13 +- .../VelopackFlowServiceClient.cs | 154 ++++++++++-------- 2 files changed, 89 insertions(+), 78 deletions(-) diff --git a/src/vpk/Velopack.Flow/FlowApiExtensions.cs b/src/vpk/Velopack.Flow/FlowApiExtensions.cs index bee79b6c..c899eb67 100644 --- a/src/vpk/Velopack.Flow/FlowApiExtensions.cs +++ b/src/vpk/Velopack.Flow/FlowApiExtensions.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Net; using System.Net.Http; using System.Text; @@ -63,16 +64,16 @@ public partial class FlowApi ProcessResponse(client, response); var status = (int) response.StatusCode; - if (status == 404) { - string responseText_ = (response.Content == null) ? string.Empty : + if (status == (int) HttpStatusCode.NotFound) { + string responseText = (response.Content == null) ? string.Empty : #if NET6_0_OR_GREATER - await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); #else - await response.Content.ReadAsStringAsync().ConfigureAwait(false); + await response.Content.ReadAsStringAsync().ConfigureAwait(false); #endif - throw new ApiException("A server side error occurred.", status, responseText_, headers, null); - } else if (status == 200 || status == 204) { + throw new ApiException("A server side error occurred.", status, responseText, headers, null); + } else if (status is (int) HttpStatusCode.OK or (int) HttpStatusCode.NoContent) { using var fs = File.Create(localFilePath); if (response.Content != null) { #if NET6_0_OR_GREATER diff --git a/src/vpk/Velopack.Flow/VelopackFlowServiceClient.cs b/src/vpk/Velopack.Flow/VelopackFlowServiceClient.cs index 69d83e35..64fbef08 100644 --- a/src/vpk/Velopack.Flow/VelopackFlowServiceClient.cs +++ b/src/vpk/Velopack.Flow/VelopackFlowServiceClient.cs @@ -10,6 +10,7 @@ using Velopack.Core.Abstractions; using Velopack.NuGet; using Velopack.Packaging; using Velopack.Util; +using System.Net; #if NET6_0_OR_GREATER using System.Net.Http.Json; @@ -17,7 +18,6 @@ using System.Net.Http.Json; using System.Net.Http; #endif -#nullable enable namespace Velopack.Flow; public class VelopackFlowServiceClient( @@ -25,6 +25,8 @@ public class VelopackFlowServiceClient( ILogger Logger, IFancyConsole Console) { + private static readonly SemanticVersion ZeroVersion = new(0, 0, 0); + private static readonly string[] Scopes = ["openid", "offline_access"]; private AuthenticationHeaderValue? Authorization = null; @@ -70,12 +72,13 @@ public class VelopackFlowServiceClient( public async Task LoginAsync(VelopackFlowLoginOptions? loginOptions, bool suppressOutput, CancellationToken cancellationToken) { loginOptions ??= new VelopackFlowLoginOptions(); - var serviceUrl = Options.VelopackBaseUrl ?? GetFlowApi().BaseUrl; + FlowApi client = GetFlowApi(); + var serviceUrl = Options.VelopackBaseUrl ?? client.BaseUrl; if (!suppressOutput) { Logger.LogInformation("Preparing to login to Velopack ({serviceUrl})", serviceUrl); } - var authConfiguration = await GetAuthConfigurationAsync(cancellationToken); + var authConfiguration = await GetAuthConfigurationAsync(client, cancellationToken); var pca = await BuildPublicApplicationAsync(authConfiguration); if (!string.IsNullOrWhiteSpace(Options.ApiKey)) { @@ -102,7 +105,7 @@ public class VelopackFlowServiceClient( Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken); } - var profile = await GetProfileAsync(cancellationToken); + var profile = await GetProfileAsync(client, cancellationToken); if (!suppressOutput) { Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName()); @@ -113,7 +116,8 @@ public class VelopackFlowServiceClient( public async Task LogoutAsync(CancellationToken cancellationToken) { - var authConfiguration = await GetAuthConfigurationAsync(cancellationToken); + FlowApi client = GetFlowApi(); + var authConfiguration = await GetAuthConfigurationAsync(client, cancellationToken); var pca = await BuildPublicApplicationAsync(authConfiguration); @@ -126,13 +130,6 @@ public class VelopackFlowServiceClient( Logger.LogInformation("Cleared saved login(s) for Velopack"); } - public async Task GetProfileAsync(CancellationToken cancellationToken) - { - AssertAuthenticated(); - var client = GetFlowApi(); - return await client.GetUserProfileAsync(cancellationToken); - } - public async Task InvokeEndpointAsync( string endpointUri, string method, @@ -183,36 +180,40 @@ public class VelopackFlowServiceClient( var packageId = fullAsset.PackageId; var version = fullAsset.Version; - var filesToUpload = assets.GetNonReleaseAssetPaths().Select(p => (p, FileType.Installer)) + var filesToUpload = assets.GetNonReleaseAssetPaths() + .Select(p => (p, FileType.Installer)) .Concat([(fullAssetPath, FileType.Release)]) .Where(kvp => !kvp.Item1.Contains("-Portable.zip")) .ToArray(); Logger.LogInformation("Beginning upload to Velopack Flow"); + FlowApi client = GetFlowApi(); + await Console.ExecuteProgressAsync( async (progress) => { ReleaseGroup releaseGroup = await progress.RunTask( $"Creating release {version}", async (report) => { report(-1); - await CreateChannelIfNotExists(packageId, channel, cancellationToken); + await CreateChannelIfNotExists(client, packageId, channel, cancellationToken); report(50); - var result = await CreateReleaseGroupAsync(packageId, version, channel, cancellationToken); + var result = await CreateReleaseGroupAsync(client, packageId, version, channel, cancellationToken); report(100); return result; }); var backgroundTasks = new List(); - foreach (var assetTuple in filesToUpload) { + foreach (var (filePath, fileType) in filesToUpload) { backgroundTasks.Add( progress.RunTask( - $"Uploading {Path.GetFileName(assetTuple.Item1)}", + $"Uploading {Path.GetFileName(filePath)}", async (report) => { await UploadReleaseAssetAsync( - assetTuple.Item1, + client, + filePath, releaseGroup.Id, - assetTuple.Item2, + fileType, report, cancellationToken); report(100); @@ -223,45 +224,52 @@ public class VelopackFlowServiceClient( using var _1 = TempUtil.GetTempDirectory(out var deltaGenTempDir); var prevVersion = Path.Combine(deltaGenTempDir, "prev.nupkg"); - var prevZip = await progress.RunTask( + ZipPackage? prevZip = await progress.RunTask( $"Downloading delta base for {version}", async (report) => { - await DownloadLatestRelease(packageId, channel, prevVersion, report, cancellationToken); - return new ZipPackage(prevVersion); + await DownloadLatestRelease(client, packageId, channel, prevVersion, report, cancellationToken); + if (File.Exists(prevVersion)) { + return new ZipPackage(prevVersion); + } + return null; }); - if (prevZip.Version! >= version) { - throw new InvalidOperationException( - $"Latest version in channel {channel} is greater than or equal to local (remote={prevZip.Version}, local={version})"); - } + if (prevZip is not null) { - var suggestedDeltaName = DefaultName.GetSuggestedReleaseName(packageId, version.ToFullString(), channel, true, RuntimeOs.Unknown); - var deltaPath = Path.Combine(releaseDirectory, suggestedDeltaName); + if (prevZip.Version! >= version) { + throw new InvalidOperationException( + $"Latest version in channel {channel} is greater than or equal to local (remote={prevZip.Version}, local={version})"); + } - await progress.RunTask( - $"Building delta {prevZip.Version} -> {version}", - (report) => { - var delta = new DeltaPackageBuilder(Logger); - var pOld = new ReleasePackage(prevVersion); - var pNew = new ReleasePackage(fullAssetPath); - delta.CreateDeltaPackage(pOld, pNew, deltaPath, DeltaMode.BestSpeed, report); - report(100); - return Task.CompletedTask; - }); + var suggestedDeltaName = DefaultName.GetSuggestedReleaseName(packageId, version.ToFullString(), channel, true, RuntimeOs.Unknown); + var deltaPath = Path.Combine(releaseDirectory, suggestedDeltaName); - backgroundTasks.Add( - progress.RunTask( - $"Uploading {Path.GetFileName(deltaPath)}", - async (report) => { - await UploadReleaseAssetAsync( - deltaPath, - releaseGroup.Id, - FileType.Release, - report, - cancellationToken); + await progress.RunTask( + $"Building delta {prevZip.Version} -> {version}", + (report) => { + var delta = new DeltaPackageBuilder(Logger); + var pOld = new ReleasePackage(prevVersion); + var pNew = new ReleasePackage(fullAssetPath); + delta.CreateDeltaPackage(pOld, pNew, deltaPath, DeltaMode.BestSpeed, report); report(100); - }) - ); + return Task.CompletedTask; + }); + + backgroundTasks.Add( + progress.RunTask( + $"Uploading {Path.GetFileName(deltaPath)}", + async (report) => { + await UploadReleaseAssetAsync( + client, + deltaPath, + releaseGroup.Id, + FileType.Release, + report, + cancellationToken); + report(100); + }) + ); + } await Task.WhenAll(backgroundTasks); @@ -269,7 +277,7 @@ public class VelopackFlowServiceClient( $"Publishing release {version}", async (report) => { report(-1); - var result = await PublishReleaseGroupAsync(releaseGroup.Id, cancellationToken); + var result = await PublishReleaseGroupAsync(client, releaseGroup.Id, cancellationToken); report(100); return result; }); @@ -279,23 +287,30 @@ public class VelopackFlowServiceClient( "Waiting for release to go live", async (report) => { report(-1); - await WaitUntilReleaseGroupLive(publishedGroup.Id, cancellationToken); + await WaitUntilReleaseGroupLive(client, publishedGroup.Id, cancellationToken); report(100); }); } }); } - private async Task DownloadLatestRelease(string packageId, string channel, string localPath, Action progress, CancellationToken cancellationToken) + + private async Task GetProfileAsync(FlowApi client, CancellationToken cancellationToken) { - var client = GetFlowApi(progress); - await client.DownloadInstallerLatestToFileAsync(packageId, channel, DownloadAssetType.Full, localPath, cancellationToken); + AssertAuthenticated(); + return await client.GetUserProfileAsync(cancellationToken); } - private async Task WaitUntilReleaseGroupLive(Guid releaseGroupId, CancellationToken cancellationToken) - { - var client = GetFlowApi(); + private static async Task DownloadLatestRelease(FlowApi client, string packageId, string channel, string localPath, Action progress, CancellationToken cancellationToken) + { + try { + await client.DownloadInstallerLatestToFileAsync(packageId, channel, DownloadAssetType.Full, localPath, cancellationToken); + } catch (ApiException e) when (e.StatusCode == (int) HttpStatusCode.NotFound) { } + } + + private async Task WaitUntilReleaseGroupLive(FlowApi client, Guid releaseGroupId, CancellationToken cancellationToken) + { for (int i = 0; i < 300; i++) { var releaseGroup = await client.GetReleaseGroupAsync(releaseGroupId, cancellationToken); if (releaseGroup?.FileUploads == null) { @@ -303,7 +318,7 @@ public class VelopackFlowServiceClient( return; } - if (releaseGroup.FileUploads.All(f => f.Status?.ToLower().Equals("processed") == true)) { + if (releaseGroup.FileUploads.All(f => f.Status?.ToLowerInvariant().Equals("processed") == true)) { Logger.LogInformation("Release is now live."); return; } @@ -314,17 +329,16 @@ public class VelopackFlowServiceClient( Logger.LogWarning("Release did not go live within 5 minutes (timeout)."); } - private async Task CreateChannelIfNotExists(string packageId, string channel, CancellationToken cancellationToken) + private static async Task CreateChannelIfNotExists(FlowApi client, string packageId, string channel, CancellationToken cancellationToken) { var request = new CreateChannelRequest() { PackageId = packageId, Name = channel, }; - var client = GetFlowApi(); await client.CreateChannelAsync(request, cancellationToken); } - private async Task CreateReleaseGroupAsync(string packageId, SemanticVersion version, string channel, CancellationToken cancellationToken) + private static async Task CreateReleaseGroupAsync(FlowApi client, string packageId, SemanticVersion version, string channel, CancellationToken cancellationToken) { CreateReleaseGroupRequest request = new() { ChannelIdentifier = channel, @@ -332,35 +346,31 @@ public class VelopackFlowServiceClient( Version = version.ToNormalizedString() }; - var client = GetFlowApi(); return await client.CreateReleaseGroupAsync(request, cancellationToken); } - private async Task UploadReleaseAssetAsync(string filePath, Guid releaseGroupId, FileType fileType, Action progress, + private static async Task UploadReleaseAssetAsync(FlowApi client, string filePath, Guid releaseGroupId, FileType fileType, Action progress, CancellationToken cancellationToken) { using var stream = File.OpenRead(filePath); var file = new FileParameter(stream); - var client = GetFlowApi(progress); await client.UploadReleaseAsync(releaseGroupId, fileType, file, cancellationToken); } - private async Task PublishReleaseGroupAsync(Guid releaseGroupId, CancellationToken cancellationToken) + private static async Task PublishReleaseGroupAsync(FlowApi client, Guid releaseGroupId, CancellationToken cancellationToken) { UpdateReleaseGroupRequest request = new() { State = ReleaseGroupState.Published }; - var client = GetFlowApi(); return await client.UpdateReleaseGroupAsync(releaseGroupId, request, cancellationToken); } - private async Task GetAuthConfigurationAsync(CancellationToken cancellationToken) + private async Task GetAuthConfigurationAsync(FlowApi client, CancellationToken cancellationToken) { - if (AuthConfiguration is not null) - return AuthConfiguration; + if (AuthConfiguration is { } authConfiguration) + return authConfiguration; - var client = GetFlowApi(); var authConfig = await client.GetV1AuthConfigAsync(cancellationToken); if (authConfig is null) @@ -372,7 +382,7 @@ public class VelopackFlowServiceClient( if (authConfig.ClientId is null) throw new Exception("Client ID not provided."); - return authConfig; + return AuthConfiguration = authConfig; } private void AssertAuthenticated()