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 <git@caesay.com>
This commit is contained in:
Kevin B
2024-07-26 07:28:14 -07:00
committed by GitHub
parent 26ea959c8a
commit 94ed3aee8a
12 changed files with 114 additions and 82 deletions

View File

@@ -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]

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<Profile?> 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<Profile>(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<ReleaseGroup> 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<ReleaseGroup>(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,
}
}

View File

@@ -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<TValue>(await response.Content.ReadAsStringAsync());
}
public static async Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
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<TValue?> ReadFromJsonAsync<TValue>(
this HttpContent content,
CancellationToken cancellationToken = default)
{
var json = await content.ReadAsStringAsync();
return Newtonsoft.Json.JsonConvert.DeserializeObject<TValue>(json);
}
}
#endif

View File

@@ -1,4 +1,4 @@
using System.Text;
using System.Text;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Json;

View File

@@ -196,7 +196,9 @@ public class Program
{
services.AddSingleton<IVelopackFlowServiceClient, VelopackFlowServiceClient>();
services.AddSingleton<HmacAuthHttpClientHandler>();
services.AddHttpClient().ConfigureHttpClientDefaults(x => x.AddHttpMessageHandler<HmacAuthHttpClientHandler>().ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromMinutes(10)));
services.AddHttpClient().ConfigureHttpClientDefaults(x =>
x.AddHttpMessageHandler<HmacAuthHttpClientHandler>()
.ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromMinutes(60)));
}
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using NuGet.Versioning;
using Velopack.Sources;
using System.Collections.Generic;

View File

@@ -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));

View File

@@ -9,7 +9,7 @@ using Velopack.Locators;
namespace Velopack.Sources
{
/// <summary>
/// Retrieves updates from the hosted Velopack service.
/// Retrieves updates from the hosted Velopack service.
/// </summary>
public sealed class VelopackFlowUpdateSource : IUpdateSource
{