From 94ed3aee8aa9a9850914c02f61a5f9bd6990261a Mon Sep 17 00:00:00 2001 From: Kevin B Date: Fri, 26 Jul 2024 07:28:14 -0700 Subject: [PATCH] Updating to use the new Velopack Flow APIs (#180) * WIP * Updated to use the new release group APIs * cleanup and increase timeouts --------- Co-authored-by: Caelan Sayler --- src/Velopack.Build/PublishTask.cs | 2 +- .../Flow/CreateReleaseGroupRequest.cs | 14 +++ src/Velopack.Packaging/Flow/ReleaseGroup.cs | 13 +++ .../Flow/UploadInstallerOptions.cs | 20 ---- src/Velopack.Packaging/Flow/UploadOptions.cs | 16 +-- .../Flow/VelopackFlowServiceClient.cs | 100 ++++++++++-------- .../HttpClientExtensions.cs | 19 ++++ src/Velopack.Packaging/ReleaseEntryHelper.cs | 2 +- src/Velopack.Vpk/Program.cs | 4 +- src/Velopack/Internal/CompiledJson.cs | 2 +- src/Velopack/NuGet/ZipPackage.cs | 2 +- .../Sources/VelopackFlowUpdateSource.cs | 2 +- 12 files changed, 114 insertions(+), 82 deletions(-) create mode 100644 src/Velopack.Packaging/Flow/CreateReleaseGroupRequest.cs create mode 100644 src/Velopack.Packaging/Flow/ReleaseGroup.cs delete mode 100644 src/Velopack.Packaging/Flow/UploadInstallerOptions.cs diff --git a/src/Velopack.Build/PublishTask.cs b/src/Velopack.Build/PublishTask.cs index 0055b939..f59467f4 100644 --- a/src/Velopack.Build/PublishTask.cs +++ b/src/Velopack.Build/PublishTask.cs @@ -13,7 +13,7 @@ public class PublishTask : MSBuildAsyncTask private static HttpClient HttpClient { get; } = new(new HmacAuthHttpClientHandler { InnerHandler = new HttpClientHandler() }) { - Timeout = TimeSpan.FromMinutes(10) + Timeout = TimeSpan.FromMinutes(60) }; [Required] diff --git a/src/Velopack.Packaging/Flow/CreateReleaseGroupRequest.cs b/src/Velopack.Packaging/Flow/CreateReleaseGroupRequest.cs new file mode 100644 index 00000000..3c68080c --- /dev/null +++ b/src/Velopack.Packaging/Flow/CreateReleaseGroupRequest.cs @@ -0,0 +1,14 @@ +#if NET6_0_OR_GREATER +#else +using System.Net.Http; +#endif + +#nullable enable +namespace Velopack.Packaging.Flow; + +internal sealed class CreateReleaseGroupRequest +{ + public string? PackageId { get; set; } + public string? Version { get; set; } + public string? ChannelIdentifier { get; set; } +} diff --git a/src/Velopack.Packaging/Flow/ReleaseGroup.cs b/src/Velopack.Packaging/Flow/ReleaseGroup.cs new file mode 100644 index 00000000..dabe60f0 --- /dev/null +++ b/src/Velopack.Packaging/Flow/ReleaseGroup.cs @@ -0,0 +1,13 @@ +#if NET6_0_OR_GREATER +#else +using System.Net.Http; +#endif + +#nullable enable +namespace Velopack.Packaging.Flow; + +internal sealed class ReleaseGroup +{ + public Guid Id { get; set; } + public string? Version { get; set; } +} \ No newline at end of file diff --git a/src/Velopack.Packaging/Flow/UploadInstallerOptions.cs b/src/Velopack.Packaging/Flow/UploadInstallerOptions.cs deleted file mode 100644 index b670884a..00000000 --- a/src/Velopack.Packaging/Flow/UploadInstallerOptions.cs +++ /dev/null @@ -1,20 +0,0 @@ - -#nullable enable - -using NuGet.Versioning; - -namespace Velopack.Packaging.Flow; - -public class UploadInstallerOptions : UploadOptions -{ - public string PackageId { get; } - - public SemanticVersion Version { get; } - - public UploadInstallerOptions(string packageId, SemanticVersion version, Stream releaseData, string fileName, string? channel) - : base(releaseData, fileName, channel) - { - PackageId = packageId; - Version = version; - } -} \ No newline at end of file diff --git a/src/Velopack.Packaging/Flow/UploadOptions.cs b/src/Velopack.Packaging/Flow/UploadOptions.cs index f9b6b178..bb918f99 100644 --- a/src/Velopack.Packaging/Flow/UploadOptions.cs +++ b/src/Velopack.Packaging/Flow/UploadOptions.cs @@ -2,17 +2,9 @@ namespace Velopack.Packaging.Flow; -public class UploadOptions : VelopackServiceOptions +public class UploadOptions(Stream releaseData, string fileName, string channel) : VelopackServiceOptions { - public Stream ReleaseData { get; } - public string FileName { get; } - public string? Channel { get; } - - public UploadOptions(Stream releaseData, string fileName, string? channel) - { - ReleaseData = releaseData; - FileName = fileName; - - Channel = channel; - } + public Stream ReleaseData { get; } = releaseData; + public string FileName { get; } = fileName; + public string Channel { get; } = channel; } diff --git a/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs b/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs index 5919b7fb..07cc1a93 100644 --- a/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs +++ b/src/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs @@ -2,6 +2,9 @@ using Microsoft.Identity.Client.Extensions.Msal; using NuGet.Versioning; using Microsoft.Extensions.Logging; +using System.Text; +using System.IO; + #if NET6_0_OR_GREATER using System.Net.Http.Json; @@ -86,7 +89,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : public async Task GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken) { AssertAuthenticated(); - var endpoint = GetEndpoint("v1/user/profile", options); + var endpoint = GetEndpoint("v1/user/profile", options?.VelopackBaseUrl); return await HttpClient.GetFromJsonAsync(endpoint, cancellationToken); } @@ -94,6 +97,8 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, RuntimeOs os, CancellationToken cancellationToken) { + AssertAuthenticated(); + channel ??= ReleaseEntryHelper.GetDefaultChannel(os); ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger, os); var latestAssets = helper.GetLatestAssets().ToList(); @@ -120,73 +125,70 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : } } - Logger.LogInformation("Uploading {AssetCount} assets to Velopack ({ServiceUrl})", latestAssets.Count + installers.Count, serviceUrl); + if (packageId is null) { + Logger.LogError("No package ID found in release directory {ReleaseDirectory}", releaseDirectory); + return; + } + if (version is null) { + Logger.LogError("No version found in release directory {ReleaseDirectory}", releaseDirectory); + return; + } + + Logger.LogInformation("Uploading {AssetCount} assets to Velopack Flow ({ServiceUrl})", latestAssets.Count + installers.Count, serviceUrl); + + ReleaseGroup releaseGroup = await CreateReleaseGroupAsync(packageId, version, channel, serviceUrl, cancellationToken); foreach (var assetFileName in files) { + await UploadReleaseAssetAsync(releaseDirectory, assetFileName, serviceUrl, releaseGroup.Id, FileType.Release, cancellationToken).ConfigureAwait(false); - var latestPath = Path.Combine(releaseDirectory, assetFileName); - - using var fileStream = File.OpenRead(latestPath); - var options = new UploadOptions(fileStream, assetFileName, channel) { - VelopackBaseUrl = serviceUrl - }; - - await UploadReleaseAssetAsync(options, cancellationToken).ConfigureAwait(false); - - Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName); + Logger.LogInformation("Uploaded {FileName} to Velopack Flow", assetFileName); } foreach (var installerFile in installers) { - var latestPath = Path.Combine(releaseDirectory, installerFile); + await UploadReleaseAssetAsync(releaseDirectory, installerFile, serviceUrl, releaseGroup.Id, FileType.Installer, cancellationToken).ConfigureAwait(false); - using var fileStream = File.OpenRead(latestPath); - var options = new UploadInstallerOptions(packageId!, version!, fileStream, installerFile, channel) { - VelopackBaseUrl = serviceUrl - }; - - await UploadInstallerAssetAsync(options, cancellationToken).ConfigureAwait(false); - - Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile); + Logger.LogInformation("Uploaded {FileName} installer to Velopack Flow", installerFile); } } - private async Task UploadReleaseAssetAsync(UploadOptions options, CancellationToken cancellationToken) + private async Task CreateReleaseGroupAsync( + string packageId, SemanticVersion version, string channel, + string? velopackBaseUrl, CancellationToken cancellationToken) { - AssertAuthenticated(); - - using var formData = new MultipartFormDataContent - { - { new StringContent(options.Channel ?? ""), "Channel" } + CreateReleaseGroupRequest request = new() { + ChannelIdentifier = channel, + PackageId = packageId, + Version = version.ToNormalizedString() }; - using var fileContent = new StreamContent(options.ReleaseData); - formData.Add(fileContent, "File", options.FileName); - - var endpoint = GetEndpoint("v1/upload-release", options); - - var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken); + var endpoint = GetEndpoint("v1/releaseGroups/create", velopackBaseUrl); + var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken); response.EnsureSuccessStatusCode(); + + return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken) + ?? throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}"); } - private async Task UploadInstallerAssetAsync(UploadInstallerOptions options, CancellationToken cancellationToken) + private async Task UploadReleaseAssetAsync(string releaseDirectory, string fileName, + string? serviceUrl, Guid releaseGroupId, FileType fileType, CancellationToken cancellationToken) { - AssertAuthenticated(); - using var formData = new MultipartFormDataContent { - { new StringContent(options.PackageId ?? ""), "PackageId" }, - { new StringContent(options.Channel ?? ""), "Channel" }, - { new StringContent(options.Version.ToNormalizedString() ?? ""), "Version" }, + { new StringContent(releaseGroupId.ToString()), "ReleaseGroupId" }, + { new StringContent(fileType.ToString()), "FileType" } }; - using var fileContent = new StreamContent(options.ReleaseData); - formData.Add(fileContent, "File", options.FileName); + var latestPath = Path.Combine(releaseDirectory, fileName); - var endpoint = GetEndpoint("v1/upload-installer", options); + using var fileStream = File.OpenRead(latestPath); + + using var fileContent = new StreamContent(fileStream); + formData.Add(fileContent, "File", fileName); + + var endpoint = GetEndpoint("v1/releases/upload", serviceUrl); var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken); - response.EnsureSuccessStatusCode(); } @@ -211,8 +213,11 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : } private static Uri GetEndpoint(string relativePath, VelopackServiceOptions? options) + => GetEndpoint(relativePath, options?.VelopackBaseUrl); + + private static Uri GetEndpoint(string relativePath, string? velopackBaseUrl) { - var baseUrl = options?.VelopackBaseUrl ?? VelopackServiceOptions.DefaultBaseUrl; + var baseUrl = velopackBaseUrl ?? VelopackServiceOptions.DefaultBaseUrl; var endpoint = new Uri(relativePath, UriKind.Relative); return new(new Uri(baseUrl), endpoint); } @@ -311,4 +316,11 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : cacheHelper.RegisterCache(pca.UserTokenCache); return pca; } + + private enum FileType + { + Unknown, + Release, + Installer, + } } diff --git a/src/Velopack.Packaging/HttpClientExtensions.cs b/src/Velopack.Packaging/HttpClientExtensions.cs index a9230d3b..cd94e41c 100644 --- a/src/Velopack.Packaging/HttpClientExtensions.cs +++ b/src/Velopack.Packaging/HttpClientExtensions.cs @@ -2,6 +2,7 @@ #nullable enable using System.Net.Http; +using System.Text; namespace Velopack.Packaging; @@ -17,5 +18,23 @@ public static class HttpClientExtensions return Newtonsoft.Json.JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); } + + public static async Task PostAsJsonAsync( + this HttpClient client, + Uri? requestUri, + TValue value, + CancellationToken cancellationToken = default) + { + var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json"); + return await client.PostAsync(requestUri, content, cancellationToken); + } + + public static async Task ReadFromJsonAsync( + this HttpContent content, + CancellationToken cancellationToken = default) + { + var json = await content.ReadAsStringAsync(); + return Newtonsoft.Json.JsonConvert.DeserializeObject(json); + } } #endif diff --git a/src/Velopack.Packaging/ReleaseEntryHelper.cs b/src/Velopack.Packaging/ReleaseEntryHelper.cs index 23e1a65c..0e49d74a 100644 --- a/src/Velopack.Packaging/ReleaseEntryHelper.cs +++ b/src/Velopack.Packaging/ReleaseEntryHelper.cs @@ -1,4 +1,4 @@ -using System.Text; +using System.Text; using Microsoft.Extensions.Logging; using NuGet.Versioning; using Velopack.Json; diff --git a/src/Velopack.Vpk/Program.cs b/src/Velopack.Vpk/Program.cs index 80673293..05d9e6a1 100644 --- a/src/Velopack.Vpk/Program.cs +++ b/src/Velopack.Vpk/Program.cs @@ -196,7 +196,9 @@ public class Program { services.AddSingleton(); services.AddSingleton(); - services.AddHttpClient().ConfigureHttpClientDefaults(x => x.AddHttpMessageHandler().ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromMinutes(10))); + services.AddHttpClient().ConfigureHttpClientDefaults(x => + x.AddHttpMessageHandler() + .ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromMinutes(60))); } } diff --git a/src/Velopack/Internal/CompiledJson.cs b/src/Velopack/Internal/CompiledJson.cs index ed9d82ad..8a278469 100644 --- a/src/Velopack/Internal/CompiledJson.cs +++ b/src/Velopack/Internal/CompiledJson.cs @@ -1,4 +1,4 @@ -using System; +using System; using NuGet.Versioning; using Velopack.Sources; using System.Collections.Generic; diff --git a/src/Velopack/NuGet/ZipPackage.cs b/src/Velopack/NuGet/ZipPackage.cs index 8089d5e7..8d2bbc34 100644 --- a/src/Velopack/NuGet/ZipPackage.cs +++ b/src/Velopack/NuGet/ZipPackage.cs @@ -46,7 +46,7 @@ namespace Velopack.NuGet return ms.ToArray(); } - private ZipArchiveEntry GetManifestEntry(ZipArchive zip) + private static ZipArchiveEntry GetManifestEntry(ZipArchive zip) { var manifest = zip.Entries .FirstOrDefault(f => f.FullName.EndsWith(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase)); diff --git a/src/Velopack/Sources/VelopackFlowUpdateSource.cs b/src/Velopack/Sources/VelopackFlowUpdateSource.cs index 782bad76..76f43b02 100644 --- a/src/Velopack/Sources/VelopackFlowUpdateSource.cs +++ b/src/Velopack/Sources/VelopackFlowUpdateSource.cs @@ -9,7 +9,7 @@ using Velopack.Locators; namespace Velopack.Sources { /// - /// Retrieves updates from the hosted Velopack service. + /// Retrieves updates from the hosted Velopack service. /// public sealed class VelopackFlowUpdateSource : IUpdateSource {