Refactor projects & add NSwag codegen

This commit is contained in:
Caelan Sayler
2024-12-24 14:36:26 +00:00
committed by Caelan
parent 2d6d155444
commit 6eb35b8a81
90 changed files with 6267 additions and 702 deletions

View File

@@ -55,6 +55,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.Build", "src\vpk\V
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Velopack.IcoLib", "src\vpk\Velopack.IcoLib\Velopack.IcoLib.csproj", "{8A0A980A-D51C-458E-8942-00BC900FD2D0}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Velopack.Flow", "src\vpk\Velopack.Flow\Velopack.Flow.csproj", "{DC836A20-E770-451A-B26E-4CC74633F741}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Velopack.Core", "src\vpk\Velopack.Core\Velopack.Core.csproj", "{2A60F8BE-EAD4-4229-A04E-52A37C8F1D0E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -121,6 +125,14 @@ Global
{8A0A980A-D51C-458E-8942-00BC900FD2D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A0A980A-D51C-458E-8942-00BC900FD2D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A0A980A-D51C-458E-8942-00BC900FD2D0}.Release|Any CPU.Build.0 = Release|Any CPU
{DC836A20-E770-451A-B26E-4CC74633F741}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DC836A20-E770-451A-B26E-4CC74633F741}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DC836A20-E770-451A-B26E-4CC74633F741}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DC836A20-E770-451A-B26E-4CC74633F741}.Release|Any CPU.Build.0 = Release|Any CPU
{2A60F8BE-EAD4-4229-A04E-52A37C8F1D0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2A60F8BE-EAD4-4229-A04E-52A37C8F1D0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2A60F8BE-EAD4-4229-A04E-52A37C8F1D0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2A60F8BE-EAD4-4229-A04E-52A37C8F1D0E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -6,6 +6,8 @@ using System.Runtime.InteropServices;
[assembly: InternalsVisibleTo("Velopack.Packaging.Tests, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack.CommandLine.Tests, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack.Core, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack.Flow, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack.Deployment, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack.Packaging, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Velopack.Packaging.Windows, PublicKey=" + SNK.SHA1)]

View File

@@ -4,7 +4,7 @@ using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Abstractions;
using Velopack.Core.Abstractions;
using ILogger = Microsoft.Extensions.Logging.ILogger;
using Task = System.Threading.Tasks.Task;

View File

@@ -1,53 +1,54 @@
using System;
using System.Net.Http;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Flow;
using Velopack.Flow;
namespace Velopack.Build;
public class PublishTask : MSBuildAsyncTask
{
private static HttpClient HttpClient { get; } = new(new HmacAuthHttpClientHandler {
InnerHandler = new HttpClientHandler()
}) {
Timeout = TimeSpan.FromMinutes(60)
};
[Required]
public string ReleaseDirectory { get; set; } = "";
public string ServiceUrl { get; set; } = VelopackServiceOptions.DefaultBaseUrl;
public string? ServiceUrl { get; set; }
public string? Channel { get; set; }
public string? ApiKey { get; set; }
public bool NoWaitForLive { get; set; }
public double Timeout { get; set; } = 30d;
public bool WaitForLive { get; set; }
protected override async Task<bool> ExecuteAsync(CancellationToken cancellationToken)
{
throw new NotImplementedException();
// //System.Diagnostics.Debugger.Launch();
// IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger, Logger);
// if (!await client.LoginAsync(new() {
// AllowDeviceCodeFlow = false,
// AllowInteractiveLogin = false,
// VelopackBaseUrl = ServiceUrl,
// ApiKey = ApiKey
// }, false, cancellationToken).ConfigureAwait(false)) {
// Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
// return true;
// }
//
// // todo: currently it's not possible to cross-compile for different OSes using Velopack.Build
// var targetOs = VelopackRuntimeInfo.SystemOs;
//
// await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl, targetOs, NoWaitForLive, cancellationToken)
// .ConfigureAwait(false);
//
// return true;
//System.Diagnostics.Debugger.Launch();
var options = new VelopackFlowServiceOptions {
VelopackBaseUrl = ServiceUrl,
ApiKey = ApiKey,
Timeout = Timeout,
};
var loginOptions = new VelopackFlowLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
};
var client = new VelopackFlowServiceClient(options, Logger, Logger);
CancellationToken token = CancellationToken.None;
if (!await client.LoginAsync(loginOptions, false, token)) {
Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login.");
return true;
}
// todo: currently it's not possible to cross-compile for different OSes using Velopack.Build
var targetOs = VelopackRuntimeInfo.SystemOs;
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, targetOs, WaitForLive, cancellationToken)
.ConfigureAwait(false);
return true;
}
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using Riok.Mapperly.Abstractions;
using Velopack.Core;
using Velopack.Packaging;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;

View File

@@ -16,6 +16,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Velopack.Flow\Velopack.Flow.csproj" />
<ProjectReference Include="..\Velopack.Packaging.Unix\Velopack.Packaging.Unix.csproj" />
<ProjectReference Include="..\Velopack.Packaging.Windows\Velopack.Packaging.Windows.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,4 @@
namespace Velopack.Packaging.Abstractions;
namespace Velopack.Core.Abstractions;
public interface ICommand<TOpt> where TOpt : class
{

View File

@@ -1,4 +1,5 @@
namespace Velopack.Packaging.Abstractions;
namespace Velopack.Core.Abstractions;
public interface IConsole
{
void WriteLine(string message = "");

View File

@@ -1,4 +1,4 @@
namespace Velopack.Packaging.Abstractions;
namespace Velopack.Core.Abstractions;
public interface IFancyConsole : IConsole
{

View File

@@ -1,4 +1,4 @@
namespace Velopack.Packaging.Abstractions;
namespace Velopack.Core.Abstractions;
public interface IFancyConsoleProgress
{

View File

@@ -1,8 +1,9 @@
using Newtonsoft.Json;
using Velopack.Packaging.Exceptions;
using Velopack.Util;
namespace Velopack.Packaging;
#nullable disable
namespace Velopack.Core;
public class BuildAssets
{
@@ -14,7 +15,7 @@ public class BuildAssets
public List<VelopackAsset> GetReleaseEntries()
{
return GetFilePaths().Where(x => x.EndsWith(".nupkg", StringComparison.OrdinalIgnoreCase))
.Select(f => VelopackAsset.FromNupkg(f))
.Select(VelopackAsset.FromNupkg)
.ToList();
}

View File

@@ -0,0 +1,55 @@
using NuGet.Versioning;
namespace Velopack.Core;
public static class DefaultName
{
public static string GetSuggestedReleaseName(string id, string version, string channel, bool delta, RuntimeOs os)
{
var suffix = GetUniqueAssetSuffix(channel);
version = SemanticVersion.Parse(version).ToNormalizedString();
if (os == RuntimeOs.Windows && channel == GetDefaultChannel(RuntimeOs.Windows)) {
return $"{id}-{version}{(delta ? "-delta" : "-full")}.nupkg";
}
return $"{id}-{version}{suffix}{(delta ? "-delta" : "-full")}.nupkg";
}
public static string GetSuggestedPortableName(string id, string channel, RuntimeOs os)
{
var suffix = GetUniqueAssetSuffix(channel);
if (os == RuntimeOs.Linux) {
if (channel == GetDefaultChannel(RuntimeOs.Linux)) {
return $"{id}.AppImage";
} else {
return $"{id}{suffix}.AppImage";
}
} else {
return $"{id}{suffix}-Portable.zip";
}
}
public static string GetSuggestedSetupName(string id, string channel, RuntimeOs os)
{
var suffix = GetUniqueAssetSuffix(channel);
if (os == RuntimeOs.Windows)
return $"{id}{suffix}-Setup.exe";
else if (os == RuntimeOs.OSX)
return $"{id}{suffix}-Setup.pkg";
else
throw new PlatformNotSupportedException("Platform not supported.");
}
private static string GetUniqueAssetSuffix(string channel)
{
return "-" + channel;
}
public static string GetDefaultChannel(RuntimeOs os)
{
if (os == RuntimeOs.Windows) return "win";
if (os == RuntimeOs.OSX) return "osx";
if (os == RuntimeOs.Linux) return "linux";
throw new NotSupportedException("Unsupported OS: " + os);
}
}

View File

@@ -4,7 +4,7 @@
using System.Net.Http;
using System.Text;
namespace Velopack.Packaging;
namespace Velopack.Core;
public static class HttpClientExtensions
{
@@ -16,7 +16,7 @@ public static class HttpClientExtensions
var response = await client.GetAsync(requestUri, cancellationToken);
response.EnsureSuccessStatusCode();
return Newtonsoft.Json.JsonConvert.DeserializeObject<TValue>(await response.Content.ReadAsStringAsync());
return SimpleJson.DeserializeObject<TValue>(await response.Content.ReadAsStringAsync());
}
public static async Task<HttpResponseMessage> PostAsJsonAsync<TValue>(
@@ -25,7 +25,7 @@ public static class HttpClientExtensions
TValue value,
CancellationToken cancellationToken = default)
{
var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json");
var content = new StringContent(SimpleJson.SerializeObject(value), Encoding.UTF8, "application/json");
return await client.PostAsync(requestUri, content, cancellationToken);
}
@@ -35,7 +35,7 @@ public static class HttpClientExtensions
TValue value,
CancellationToken cancellationToken = default)
{
var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json");
var content = new StringContent(SimpleJson.SerializeObject(value), Encoding.UTF8, "application/json");
return await client.PutAsync(requestUri, content, cancellationToken);
}
@@ -44,7 +44,7 @@ public static class HttpClientExtensions
CancellationToken cancellationToken = default)
{
var json = await content.ReadAsStringAsync();
return Newtonsoft.Json.JsonConvert.DeserializeObject<TValue>(json);
return SimpleJson.DeserializeObject<TValue>(json);
}
public static async Task<string> ReadAsStringAsync(this HttpContent content, CancellationToken _)

View File

@@ -2,7 +2,7 @@
using Newtonsoft.Json.Converters;
using NuGet.Versioning;
namespace Velopack.Packaging;
namespace Velopack.Core;
public class SimpleJson
{
@@ -11,7 +11,7 @@ public class SimpleJson
NullValueHandling = NullValueHandling.Ignore,
};
public static T DeserializeObject<T>(string json)
public static T? DeserializeObject<T>(string json)
{
return JsonConvert.DeserializeObject<T>(json, Options);
}
@@ -23,15 +23,15 @@ public class SimpleJson
private class SemanticVersionConverter : JsonConverter<SemanticVersion>
{
public override SemanticVersion ReadJson(JsonReader reader, Type objectType, SemanticVersion existingValue, bool hasExistingValue,
public override SemanticVersion? ReadJson(JsonReader reader, Type objectType, SemanticVersion? existingValue, bool hasExistingValue,
JsonSerializer serializer)
{
string s = reader.Value as string;
string? s = reader.Value as string;
if (s == null) return null;
return SemanticVersion.Parse(s);
}
public override void WriteJson(JsonWriter writer, SemanticVersion value, JsonSerializer serializer)
public override void WriteJson(JsonWriter writer, SemanticVersion? value, JsonSerializer serializer)
{
if (value != null) {
writer.WriteValue(value.ToFullString());

View File

@@ -1,7 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
namespace Velopack.Packaging.Exceptions;
namespace Velopack.Core;
/// <summary>
/// Denotes that an error has occurred for which a stack trace should not be printed.

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\lib-csharp\Velopack.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using System.Text;
using Microsoft.Extensions.Logging;
using Octokit;
using Velopack.Core;
using Velopack.NuGet;
using Velopack.Packaging;
using Velopack.Packaging.Exceptions;
@@ -131,7 +132,7 @@ public class GitHubRepository(ILogger logger) : SourceRepository<GitHubDownloadO
},
"Uploading " + releasesFileName);
if (options.Channel == ReleaseEntryHelper.GetDefaultChannel(RuntimeOs.Windows)) {
if (options.Channel == DefaultName.GetDefaultChannel(RuntimeOs.Windows)) {
var legacyReleasesContent = ReleaseEntryHelper.GetLegacyMigrationReleaseFeedString(feed);
var legacyReleasesBytes = Encoding.UTF8.GetBytes(legacyReleasesContent);
await RetryAsync(

View File

@@ -4,9 +4,9 @@ using Gitea.Net.Api;
using Gitea.Net.Client;
using Gitea.Net.Model;
using Microsoft.Extensions.Logging;
using Velopack.Core;
using Velopack.NuGet;
using Velopack.Packaging;
using Velopack.Packaging.Exceptions;
using Velopack.Sources;
using Velopack.Util;
@@ -155,7 +155,7 @@ public class GiteaRepository : SourceRepository<GiteaDownloadOptions, GiteaSourc
await apiInstance.RepoCreateReleaseAttachmentAsync(repoOwner, repoName, release.Id, releasesFileName, new MemoryStream(Encoding.UTF8.GetBytes(json)));
}, "Uploading " + releasesFileName);
if (options.Channel == ReleaseEntryHelper.GetDefaultChannel(RuntimeOs.Windows)) {
if (options.Channel == DefaultName.GetDefaultChannel(RuntimeOs.Windows)) {
var legacyReleasesContent = ReleaseEntryHelper.GetLegacyMigrationReleaseFeedString(feed);
var legacyReleasesBytes = Encoding.UTF8.GetBytes(legacyReleasesContent);
await RetryAsync(async () => {

View File

@@ -15,6 +15,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Velopack.Core\Velopack.Core.csproj" />
<ProjectReference Include="..\Velopack.Packaging\Velopack.Packaging.csproj" />
</ItemGroup>

View File

@@ -1,5 +1,6 @@
using System.Text;
using Microsoft.Extensions.Logging;
using Velopack.Core;
using Velopack.Packaging;
using Velopack.Util;

View File

@@ -1,6 +1,5 @@
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Velopack.Packaging;
using Microsoft.Extensions.Logging;
using Velopack.Core;
using Velopack.Packaging.Abstractions;
using Velopack.Sources;
using Velopack.Util;
@@ -14,7 +13,7 @@ public class RepositoryOptions : IOutputOptions
public RuntimeOs TargetOs { get; set; }
public string Channel {
get => _channel ?? ReleaseEntryHelper.GetDefaultChannel(TargetOs);
get => _channel ?? DefaultName.GetDefaultChannel(TargetOs);
set => _channel = value;
}

View File

@@ -0,0 +1,25 @@
using Microsoft.Extensions.Logging;
using Velopack.Core.Abstractions;
namespace Velopack.Flow.Commands;
public class ApiCommandRunner(ILogger logger, IFancyConsole console) : ICommand<ApiOptions>
{
public async Task Run(ApiOptions options)
{
var loginOptions = new VelopackFlowLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
};
var client = new VelopackFlowServiceClient(options, logger, console);
CancellationToken token = CancellationToken.None;
if (!await client.LoginAsync(loginOptions, false, token)) {
return;
}
string response = await client.InvokeEndpointAsync(options, options.Endpoint, options.Method, options.Body, token);
Console.WriteLine(response);
}
}

View File

@@ -0,0 +1,8 @@
namespace Velopack.Flow.Commands;
public sealed class ApiOptions : VelopackFlowServiceOptions
{
public string Endpoint { get; set; } = "";
public string Method { get; set; } = "";
public string? Body { get; set; }
}

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.Logging;
using Velopack.Core.Abstractions;
namespace Velopack.Flow.Commands;
public class LoginCommandRunner(ILogger logger, IFancyConsole console) : ICommand<LoginOptions>
{
public async Task Run(LoginOptions options)
{
var client = new VelopackFlowServiceClient(options, logger, console);
await client.LoginAsync(null, false, CancellationToken.None);
}
}

View File

@@ -0,0 +1,3 @@
namespace Velopack.Flow.Commands;
public sealed class LoginOptions : VelopackFlowServiceOptions;

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.Logging;
using Velopack.Core.Abstractions;
namespace Velopack.Flow.Commands;
public class LogoutCommandRunner(ILogger logger, IFancyConsole console) : ICommand<LogoutOptions>
{
public async Task Run(LogoutOptions options)
{
var client = new VelopackFlowServiceClient(options, logger, console);
await client.LogoutAsync(CancellationToken.None);
}
}

View File

@@ -0,0 +1,3 @@
namespace Velopack.Flow.Commands;
public sealed class LogoutOptions : VelopackFlowServiceOptions;

View File

@@ -0,0 +1,29 @@
using Microsoft.Extensions.Logging;
using Velopack.Core.Abstractions;
namespace Velopack.Flow.Commands;
public class PublishCommandRunner(ILogger logger, IFancyConsole console) : ICommand<PublishOptions>
{
public async Task Run(PublishOptions options)
{
var loginOptions = new VelopackFlowLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
};
var client = new VelopackFlowServiceClient(options, logger, console);
CancellationToken token = CancellationToken.None;
if (!await client.LoginAsync(loginOptions, false, token)) {
return;
}
await client.UploadLatestReleaseAssetsAsync(
options.Channel,
options.ReleaseDirectory,
options.TargetOs,
options.WaitForLive,
token);
}
}

View File

@@ -0,0 +1,12 @@
namespace Velopack.Flow.Commands;
public sealed class PublishOptions : VelopackFlowServiceOptions
{
public RuntimeOs TargetOs { get; set; }
public string ReleaseDirectory { get; set; } = "";
public string? Channel { get; set; }
public bool WaitForLive { get; set; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
dotnet tool update --global NSwag.ConsoleCore
nswag openapi2csclient /input:https://api.velopack.io/swagger/v1/swagger.json /output:FlowApi.cs /namespace:Velopack.Flow /classname:FlowApi

View File

@@ -0,0 +1,98 @@
using System.Globalization;
using System.Net.Http;
using System.Text;
namespace Velopack.Flow;
public partial class Profile
{
public string GetDisplayName()
{
return DisplayName ?? Email ?? "<unknown>";
}
}
public partial class FlowApi
{
public virtual async Task DownloadInstallerLatestToFileAsync(string packageId, string channel, DownloadAssetType? assetType, string localFilePath,
CancellationToken cancellationToken)
{
if (packageId == null)
throw new ArgumentNullException("packageId");
if (channel == null)
throw new ArgumentNullException("channel");
var client_ = _httpClient;
var disposeClient_ = false;
try {
using (var request_ = new HttpRequestMessage()) {
request_.Method = new HttpMethod("GET");
var urlBuilder_ = new StringBuilder();
if (!string.IsNullOrEmpty(_baseUrl)) urlBuilder_.Append(_baseUrl);
// Operation Path: "v1/download/{packageId}/{channel}"
urlBuilder_.Append("v1/download/");
urlBuilder_.Append(Uri.EscapeDataString(ConvertToString(packageId, CultureInfo.InvariantCulture)));
urlBuilder_.Append('/');
urlBuilder_.Append(Uri.EscapeDataString(ConvertToString(channel, CultureInfo.InvariantCulture)));
urlBuilder_.Append('?');
if (assetType != null) {
urlBuilder_.Append(Uri.EscapeDataString("assetType")).Append('=')
.Append(Uri.EscapeDataString(ConvertToString(assetType, CultureInfo.InvariantCulture))).Append('&');
}
urlBuilder_.Length--;
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new Uri(url_, UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var disposeResponse_ = true;
try {
var headers_ = new Dictionary<string, IEnumerable<string>>();
foreach (var item_ in response_.Headers)
headers_[item_.Key] = item_.Value;
if (response_.Content != null && response_.Content.Headers != null) {
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = (int) response_.StatusCode;
if (status_ == 404) {
string responseText_ = (response_.Content == null) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("A server side error occurred.", status_, responseText_, headers_, null);
} else if (status_ == 200 || status_ == 204) {
using var fs = File.Create(localFilePath);
#if NET6_0_OR_GREATER
await response_.Content.CopyToAsync(fs, cancellationToken);
#else
await response_.Content.CopyToAsync(fs);
#endif
return;
} else {
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException(
"The HTTP status code of the response was not expected (" + status_ + ").",
status_,
responseData_,
headers_,
null);
}
} finally {
if (disposeResponse_)
response_.Dispose();
}
}
} finally {
if (disposeClient_)
client_.Dispose();
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Net.Http;
using System.Net.Http.Headers;
namespace Velopack.Packaging.Flow;
namespace Velopack.Flow;
public class HmacAuthHttpClientHandler : DelegatingHandler
{

View File

@@ -1,9 +1,7 @@
using System.Security.Cryptography;
using System.Text;
#nullable enable
namespace Velopack.Packaging.Flow;
namespace Velopack.Flow;
public static class HmacHelper
{
@@ -11,7 +9,7 @@ public static class HmacHelper
public static DateTime EpochStart { get; } = new(1970, 01, 01, 0, 0, 0, 0, DateTimeKind.Utc);
public static uint GetSecondsSinceEpoch()
=> (uint)(DateTime.UtcNow - EpochStart).TotalSeconds;
=> (uint) (DateTime.UtcNow - EpochStart).TotalSeconds;
public static string BuildSignature(string hashedId, string httpMethod, string requestUri, uint secondsSinceEpoch, string nonce)
=> $"{hashedId}{httpMethod.ToUpperInvariant()}{requestUri.ToLowerInvariant()}{secondsSinceEpoch}{nonce}";
@@ -45,6 +43,7 @@ public static class HmacHelper
if (signatureData is null) {
throw new ArgumentNullException(nameof(signatureData));
}
using HMAC hmac = new HMACSHA256();
hmac.Key = secret ?? throw new ArgumentNullException(nameof(secret));
return Convert.ToBase64String(hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureData)));

View File

@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net472;net6.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.66.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" Aliases="HttpFormatting" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Velopack.Core\Velopack.Core.csproj" />
<ProjectReference Include="..\Velopack.Packaging\Velopack.Packaging.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,9 +1,8 @@
#nullable enable
namespace Velopack.Packaging.Flow;
namespace Velopack.Flow;
public class VelopackLoginOptions : VelopackServiceOptions
public class VelopackFlowLoginOptions
{
public bool AllowCacheCredentials { get; set; } = true;
public bool AllowInteractiveLogin { get; set; } = true;
public bool AllowDeviceCodeFlow { get; set; } = true;
}
}

View File

@@ -5,8 +5,10 @@ using NuGet.Versioning;
using Microsoft.Extensions.Logging;
using System.Text;
using System.Net.Http.Headers;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.NuGet;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging;
using Velopack.Util;
#if NET6_0_OR_GREATER
@@ -16,29 +18,12 @@ using System.Net.Http;
#endif
#nullable enable
namespace Velopack.Packaging.Flow;
public interface IVelopackFlowServiceClient
{
Task<bool> LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken);
Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task<Profile?> GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task<string> InvokeEndpointAsync(VelopackServiceOptions? options, string endpointUri,
string method,
string? body,
CancellationToken cancellationToken);
Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, RuntimeOs os,
bool noWaitForLive, CancellationToken cancellationToken);
}
namespace Velopack.Flow;
public class VelopackFlowServiceClient(
IHttpMessageHandlerFactory HttpMessageHandlerFactory,
VelopackFlowServiceOptions Options,
ILogger Logger,
IFancyConsole Console) : IVelopackFlowServiceClient
IFancyConsole Console)
{
private static readonly string[] Scopes = ["openid", "offline_access"];
@@ -48,7 +33,7 @@ public class VelopackFlowServiceClient(
private HttpClient GetHttpClient(Action<int>? progress = null)
{
HttpMessageHandler primaryHandler = HttpMessageHandlerFactory.CreateHandler("flow");
HttpMessageHandler primaryHandler = new HmacAuthHttpClientHandler();
if (progress != null) {
var ph = new HttpFormatting::System.Net.Http.Handlers.ProgressMessageHandler(primaryHandler);
@@ -59,37 +44,51 @@ public class VelopackFlowServiceClient(
ph.HttpReceiveProgress += (_, args) => {
progress(args.ProgressPercentage);
};
primaryHandler = ph;
}
var client = new HttpClient(primaryHandler);
client.DefaultRequestHeaders.Authorization = Authorization;
client.Timeout = TimeSpan.FromMinutes(Options.Timeout);
return client;
}
public async Task<bool> LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken)
private FlowApi GetFlowApi(Action<int>? progress = null)
{
options ??= new VelopackLoginOptions();
if (!suppressOutput) {
Logger.LogInformation("Preparing to login to Velopack ({ServiceUrl})", options.VelopackBaseUrl);
var client = GetHttpClient(progress);
var api = new FlowApi(client);
if (!String.IsNullOrWhiteSpace(Options.VelopackBaseUrl)) {
api.BaseUrl = Options.VelopackBaseUrl;
}
var authConfiguration = await GetAuthConfigurationAsync(options, cancellationToken);
return api;
}
public async Task<bool> LoginAsync(VelopackFlowLoginOptions? loginOptions, bool suppressOutput, CancellationToken cancellationToken)
{
loginOptions ??= new VelopackFlowLoginOptions();
var serviceUrl = Options.VelopackBaseUrl ?? GetFlowApi().BaseUrl;
if (!suppressOutput) {
Logger.LogInformation("Preparing to login to Velopack ({serviceUrl})", serviceUrl);
}
var authConfiguration = await GetAuthConfigurationAsync(cancellationToken);
var pca = await BuildPublicApplicationAsync(authConfiguration);
if (!string.IsNullOrWhiteSpace(options.ApiKey)) {
Authorization = new(HmacHelper.HmacScheme, options.ApiKey);
if (!string.IsNullOrWhiteSpace(Options.ApiKey)) {
Authorization = new(HmacHelper.HmacScheme, Options.ApiKey);
} else {
AuthenticationResult? rv = null;
if (options.AllowCacheCredentials) {
if (loginOptions.AllowCacheCredentials) {
rv = await AcquireSilentlyAsync(pca, cancellationToken);
}
if (rv is null && options.AllowInteractiveLogin) {
if (rv is null && loginOptions.AllowInteractiveLogin) {
rv = await AcquireInteractiveAsync(pca, authConfiguration, cancellationToken);
}
if (rv is null && options.AllowDeviceCodeFlow) {
if (rv is null && loginOptions.AllowDeviceCodeFlow) {
rv = await AcquireByDeviceCodeAsync(pca, cancellationToken);
}
@@ -101,7 +100,7 @@ public class VelopackFlowServiceClient(
Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken);
}
var profile = await GetProfileAsync(options, cancellationToken);
var profile = await GetProfileAsync(cancellationToken);
if (!suppressOutput) {
Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName());
@@ -110,9 +109,9 @@ public class VelopackFlowServiceClient(
return true;
}
public async Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
public async Task LogoutAsync(CancellationToken cancellationToken)
{
var authConfiguration = await GetAuthConfigurationAsync(options, cancellationToken);
var authConfiguration = await GetAuthConfigurationAsync(cancellationToken);
var pca = await BuildPublicApplicationAsync(authConfiguration);
@@ -125,31 +124,30 @@ public class VelopackFlowServiceClient(
Logger.LogInformation("Cleared saved login(s) for Velopack");
}
public async Task<Profile?> GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
public async Task<Profile?> GetProfileAsync(CancellationToken cancellationToken)
{
AssertAuthenticated();
var endpoint = GetEndpoint("v1/user/profile", options?.VelopackBaseUrl);
var client = GetHttpClient();
return await client.GetFromJsonAsync<Profile>(endpoint, cancellationToken);
var client = GetFlowApi();
return await client.GetUserProfileAsync(cancellationToken);
}
public async Task<string> InvokeEndpointAsync(
VelopackServiceOptions? options,
VelopackFlowServiceOptions? options,
string endpointUri,
string method,
string? body,
CancellationToken cancellationToken)
{
AssertAuthenticated();
var endpoint = GetEndpoint(endpointUri, options?.VelopackBaseUrl);
var client = GetHttpClient();
var endpoint = new FlowApi(client).BaseUrl;
HttpRequestMessage request = new(new HttpMethod(method), endpoint);
if (body is not null) {
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
var client = GetHttpClient();
HttpResponseMessage response = await client.SendAsync(request, cancellationToken);
#if NET6_0_OR_GREATER
@@ -166,14 +164,14 @@ public class VelopackFlowServiceClient(
}
}
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl,
RuntimeOs os, bool noWaitForLive, CancellationToken cancellationToken)
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory,
RuntimeOs os, bool waitForLive, CancellationToken cancellationToken)
{
AssertAuthenticated();
channel ??= ReleaseEntryHelper.GetDefaultChannel(os);
channel ??= DefaultName.GetDefaultChannel(os);
BuildAssets assets = BuildAssets.Read(releaseDirectory, channel);
var fullAsset = assets.GetReleaseEntries().SingleOrDefault(a => a.Type == VelopackAssetType.Full);
var fullAsset = assets.GetReleaseEntries().SingleOrDefault(a => a.Type == Velopack.VelopackAssetType.Full);
if (fullAsset is null) {
Logger.LogError("No full asset found in release directory {ReleaseDirectory} (or it's missing from assets file)", releaseDirectory);
@@ -188,7 +186,7 @@ public class VelopackFlowServiceClient(
.Concat([(fullAssetPath, FileType.Release)])
.ToArray();
Logger.LogInformation("Beginning upload to Velopack Flow (serviceUrl={ServiceUrl})", serviceUrl);
Logger.LogInformation("Beginning upload to Velopack Flow");
await Console.ExecuteProgressAsync(
async (progress) => {
@@ -196,7 +194,7 @@ public class VelopackFlowServiceClient(
$"Creating release {version}",
async (report) => {
report(-1);
var result = await CreateReleaseGroupAsync(packageId, version, channel, serviceUrl, cancellationToken);
var result = await CreateReleaseGroupAsync(packageId, version, channel, cancellationToken);
report(100);
return result;
});
@@ -209,7 +207,6 @@ public class VelopackFlowServiceClient(
async (report) => {
await UploadReleaseAssetAsync(
assetTuple.Item1,
serviceUrl,
releaseGroup.Id,
assetTuple.Item2,
report,
@@ -225,7 +222,7 @@ public class VelopackFlowServiceClient(
var prevZip = await progress.RunTask(
$"Downloading delta base for {version}",
async (report) => {
await DownloadLatestRelease(packageId, channel, serviceUrl, prevVersion, report, cancellationToken);
await DownloadLatestRelease(packageId, channel, prevVersion, report, cancellationToken);
return new ZipPackage(prevVersion);
});
@@ -234,7 +231,7 @@ public class VelopackFlowServiceClient(
$"Latest version in channel {channel} is greater than or equal to local (remote={prevZip.Version}, local={version})");
}
var suggestedDeltaName = ReleaseEntryHelper.GetSuggestedReleaseName(packageId, version.ToFullString(), channel, true, RuntimeOs.Unknown);
var suggestedDeltaName = DefaultName.GetSuggestedReleaseName(packageId, version.ToFullString(), channel, true, RuntimeOs.Unknown);
var deltaPath = Path.Combine(releaseDirectory, suggestedDeltaName);
await progress.RunTask(
@@ -254,7 +251,6 @@ public class VelopackFlowServiceClient(
async (report) => {
await UploadReleaseAssetAsync(
deltaPath,
serviceUrl,
releaseGroup.Id,
FileType.Release,
report,
@@ -269,50 +265,35 @@ public class VelopackFlowServiceClient(
$"Publishing release {version}",
async (report) => {
report(-1);
var result = await PublishReleaseGroupAsync(releaseGroup, serviceUrl, cancellationToken);
var result = await PublishReleaseGroupAsync(releaseGroup.Id, cancellationToken);
report(100);
return result;
});
if (!noWaitForLive) {
if (waitForLive) {
await progress.RunTask(
"Waiting for release to go live",
async (report) => {
report(-1);
await WaitUntilReleaseGroupLive(publishedGroup.Id, serviceUrl, cancellationToken);
await WaitUntilReleaseGroupLive(publishedGroup.Id, cancellationToken);
report(100);
});
}
});
}
private async Task DownloadLatestRelease(string packageId, string channel, string? velopackBaseUrl, string localPath,
Action<int> progress, CancellationToken cancellationToken)
private async Task DownloadLatestRelease(string packageId, string channel, string localPath, Action<int> progress, CancellationToken cancellationToken)
{
var client = GetHttpClient(progress);
var endpoint = GetEndpoint($"v1/download/{packageId}/{channel}", velopackBaseUrl) + $"?assetType=Full";
using var fs = File.Create(localPath);
var response = await client.GetAsync(endpoint, cancellationToken);
response.EnsureSuccessStatusCode();
#if NET6_0_OR_GREATER
await response.Content.CopyToAsync(fs, cancellationToken);
#else
await response.Content.CopyToAsync(fs);
#endif
var client = GetFlowApi(progress);
await client.DownloadInstallerLatestToFileAsync(packageId, channel, DownloadAssetType.Full, localPath, cancellationToken);
}
private async Task WaitUntilReleaseGroupLive(Guid releaseGroupId, string? velopackBaseUrl, CancellationToken cancellationToken)
private async Task WaitUntilReleaseGroupLive(Guid releaseGroupId, CancellationToken cancellationToken)
{
var client = GetHttpClient();
var endpoint = GetEndpoint($"v1/releaseGroups/{releaseGroupId}", velopackBaseUrl);
var client = GetFlowApi();
for (int i = 0; i < 300; i++) {
var response = await client.GetAsync(endpoint, cancellationToken);
response.EnsureSuccessStatusCode();
var releaseGroup = await response.Content.ReadFromJsonAsync<ReleaseGroup>(cancellationToken: cancellationToken);
var releaseGroup = await client.GetReleaseGroupAsync(releaseGroupId, cancellationToken);
if (releaseGroup?.FileUploads == null) {
Logger.LogWarning("Failed to get release group status, it may not be live yet.");
return;
@@ -329,9 +310,7 @@ public class VelopackFlowServiceClient(
Logger.LogWarning("Release did not go live within 5 minutes (timeout).");
}
private async Task<ReleaseGroup> CreateReleaseGroupAsync(
string packageId, SemanticVersion version, string channel,
string? velopackBaseUrl, CancellationToken cancellationToken)
private async Task<ReleaseGroup> CreateReleaseGroupAsync(string packageId, SemanticVersion version, string channel, CancellationToken cancellationToken)
{
CreateReleaseGroupRequest request = new() {
ChannelIdentifier = channel,
@@ -339,70 +318,37 @@ public class VelopackFlowServiceClient(
Version = version.ToNormalizedString()
};
var client = GetHttpClient();
var endpoint = GetEndpoint("v1/releaseGroups/create", velopackBaseUrl);
var response = await client.PostAsJsonAsync(endpoint, request, cancellationToken);
if (!response.IsSuccessStatusCode) {
string content = await response.Content.ReadAsStringAsync(cancellationToken);
throw new InvalidOperationException(
$"Failed to create release group with version {version.ToNormalizedString()}" +
$"{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}");
}
return await response.Content.ReadFromJsonAsync<ReleaseGroup>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}");
var client = GetFlowApi();
return await client.CreateReleaseGroupAsync(request, cancellationToken);
}
private async Task UploadReleaseAssetAsync(string filePath, string? velopackBaseUrl, Guid releaseGroupId,
FileType fileType, Action<int> progress, CancellationToken cancellationToken)
private async Task UploadReleaseAssetAsync(string filePath, Guid releaseGroupId, FileType fileType, Action<int> progress,
CancellationToken cancellationToken)
{
using var formData = new MultipartFormDataContent();
formData.Add(new StringContent(releaseGroupId.ToString()), "ReleaseGroupId");
formData.Add(new StringContent(fileType.ToString()), "FileType");
using var fileStream = File.OpenRead(filePath);
using var fileContent = new StreamContent(fileStream);
formData.Add(fileContent, "File", Path.GetFileName(filePath));
var endpoint = GetEndpoint("v1/releases/upload", velopackBaseUrl);
var client = GetHttpClient(progress);
var response = await client.PostAsync(endpoint, formData, cancellationToken);
response.EnsureSuccessStatusCode();
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<ReleaseGroup> PublishReleaseGroupAsync(
ReleaseGroup releaseGroup, string? velopackBaseUrl, CancellationToken cancellationToken)
private async Task<ReleaseGroup> PublishReleaseGroupAsync(Guid releaseGroupId, CancellationToken cancellationToken)
{
UpdateReleaseGroupRequest request = new() {
State = ReleaseGroupState.Published
};
var client = GetHttpClient();
var endpoint = GetEndpoint($"v1/releaseGroups/{releaseGroup.Id}", velopackBaseUrl);
var response = await client.PutAsJsonAsync(endpoint, request, cancellationToken);
if (!response.IsSuccessStatusCode) {
string content = await response.Content.ReadAsStringAsync(cancellationToken);
throw new InvalidOperationException(
$"Failed to publish release group with id {releaseGroup.Id}.{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}");
}
return await response.Content.ReadFromJsonAsync<ReleaseGroup>(cancellationToken: cancellationToken)
?? throw new InvalidOperationException($"Failed to publish release group with id {releaseGroup.Id}");
var client = GetFlowApi();
return await client.UpdateReleaseGroupAsync(releaseGroupId, request, cancellationToken);
}
private async Task<AuthConfiguration> GetAuthConfigurationAsync(VelopackServiceOptions? options, CancellationToken cancellationToken)
private async Task<AuthConfiguration> GetAuthConfigurationAsync(CancellationToken cancellationToken)
{
if (AuthConfiguration is not null)
return AuthConfiguration;
var endpoint = GetEndpoint("v1/auth/config", options);
var client = GetFlowApi();
var authConfig = await client.GetV1AuthConfigAsync(cancellationToken);
var client = GetHttpClient();
var authConfig = await client.GetFromJsonAsync<AuthConfiguration>(endpoint, cancellationToken);
if (authConfig is null)
throw new Exception("Failed to get auth configuration.");
if (authConfig.B2CAuthority is null)
@@ -415,16 +361,6 @@ public class VelopackFlowServiceClient(
return authConfig;
}
private static Uri GetEndpoint(string relativePath, VelopackServiceOptions? options)
=> GetEndpoint(relativePath, options?.VelopackBaseUrl);
private static Uri GetEndpoint(string relativePath, string? velopackBaseUrl)
{
var baseUrl = velopackBaseUrl ?? VelopackServiceOptions.DefaultBaseUrl;
var endpoint = new Uri(relativePath, UriKind.Relative);
return new(new Uri(baseUrl), endpoint);
}
private void AssertAuthenticated()
{
if (Authorization is null) {
@@ -516,7 +452,7 @@ public class VelopackFlowServiceClient(
.WithB2CAuthority(authConfiguration.B2CAuthority)
.WithRedirectUri(authConfiguration.RedirectUri)
#if DEBUG
.WithLogging((Microsoft.Identity.Client.LogLevel level, string message, bool containsPii) => System.Console.WriteLine($"[{level}]: {message}"), enablePiiLogging: true, enableDefaultPlatformLogging: true)
.WithLogging((Microsoft.Identity.Client.LogLevel level, string message, bool containsPii) => System.Console.WriteLine($"[{level}]: {message}"), enablePiiLogging: true, enableDefaultPlatformLogging: true)
#endif
.WithClientName("velopack")
.Build();
@@ -524,11 +460,4 @@ public class VelopackFlowServiceClient(
cacheHelper.RegisterCache(pca.UserTokenCache);
return pca;
}
private enum FileType
{
Unknown,
Release,
Installer,
}
}

View File

@@ -0,0 +1,10 @@
namespace Velopack.Flow;
public class VelopackFlowServiceOptions
{
public string? VelopackBaseUrl { get; set; } = string.Empty;
public string? ApiKey { get; set; } = string.Empty;
public double Timeout { get; set; } = 30d;
}

View File

@@ -1,6 +1,7 @@
using ELFSharp.ELF;
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Abstractions;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Packaging.Unix.Commands;

View File

@@ -2,8 +2,8 @@
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Exceptions;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Packaging.Unix.Commands;

View File

@@ -1,7 +1,6 @@
using System.Collections.Concurrent;
using System.Runtime.Versioning;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Abstractions;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Packaging.Unix.Commands;

View File

@@ -1,6 +1,7 @@
using System.Runtime.Versioning;
using System.Security;
using Microsoft.Extensions.Logging;
using Velopack.Core;
using Velopack.Util;
namespace Velopack.Packaging.Unix;

View File

@@ -1,7 +1,7 @@
using System.Diagnostics;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Exceptions;
using Velopack.Core;
using Velopack.Util;
namespace Velopack.Packaging.Windows;

View File

@@ -1,11 +1,9 @@
using System.IO.Compression;
using System.Runtime.Versioning;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using Velopack.Compression;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.NuGet;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.NuGet;
using Velopack.Util;
using Velopack.Windows;

View File

@@ -1,4 +1,6 @@
namespace Velopack.Packaging.Windows.Commands;
using Velopack.Core;
namespace Velopack.Packaging.Windows.Commands;
public class WindowsReleasifyOptions : WindowsSigningOptions
{

View File

@@ -6,8 +6,8 @@ using AsmResolver.PE.DotNet.Cil;
using AsmResolver.PE.Win32Resources.Version;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Exceptions;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Packaging.Windows;

View File

@@ -2,5 +2,5 @@
public interface IPlatformOptions : IOutputOptions
{
RID TargetRuntime { get; set; }
RID? TargetRuntime { get; set; }
}

View File

@@ -1,5 +1,5 @@
using Microsoft.Extensions.Logging;
using Velopack.Packaging.Abstractions;
using Velopack.Core.Abstractions;
namespace Velopack.Packaging.Commands;
@@ -16,15 +16,18 @@ public class DeltaGenCommandRunner : ICommand<DeltaGenOptions>
public async Task Run(DeltaGenOptions options)
{
await _console.ExecuteProgressAsync(async (ctx) => {
var pold = new ReleasePackage(options.BasePackage);
var pnew = new ReleasePackage(options.NewPackage);
await ctx.RunTask($"Building delta {pold.Version} -> {pnew.Version}", (progress) => {
var delta = new DeltaPackageBuilder(_logger);
delta.CreateDeltaPackage(pold, pnew, options.OutputFile, options.DeltaMode, progress);
progress(100);
return Task.CompletedTask;
await _console.ExecuteProgressAsync(
async (ctx) => {
var pold = new ReleasePackage(options.BasePackage);
var pnew = new ReleasePackage(options.NewPackage);
await ctx.RunTask(
$"Building delta {pold.Version} -> {pnew.Version}",
(progress) => {
var delta = new DeltaPackageBuilder(_logger);
delta.CreateDeltaPackage(pold, pnew, options.OutputFile, options.DeltaMode, progress);
progress(100);
return Task.CompletedTask;
});
});
});
}
}
}

View File

@@ -1,4 +1,6 @@
namespace Velopack.Packaging.Commands;
using Velopack.Core;
namespace Velopack.Packaging.Commands;
public class DeltaGenOptions
{
@@ -9,4 +11,4 @@ public class DeltaGenOptions
public string NewPackage { get; set; }
public string OutputFile { get; set; }
}
}

View File

@@ -1,7 +1,7 @@
using Microsoft.Extensions.Logging;
using Velopack.Compression;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Abstractions;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Packaging.Commands;
@@ -35,18 +35,24 @@ public class DeltaPatchCommandRunner : ICommand<DeltaPatchOptions>
var delta = new DeltaEmbedded(HelperFile.GetZstdPath(), _logger, tmp);
EasyZip.ExtractZipToDirectory(_logger, options.BasePackage, workDir);
await _console.ExecuteProgressAsync(async (ctx) => {
foreach (var f in options.PatchFiles) {
await ctx.RunTask($"Applying {f.Name}", (progress) => {
delta.ApplyDeltaPackageFast(workDir, f.FullName, progress);
progress(100);
return Task.CompletedTask;
});
}
await ctx.RunTask($"Building {Path.GetFileName(options.OutputFile)}", async (progress) => {
await EasyZip.CreateZipFromDirectoryAsync(_logger, options.OutputFile, workDir, progress);
progress(100);
await _console.ExecuteProgressAsync(
async (ctx) => {
foreach (var f in options.PatchFiles) {
await ctx.RunTask(
$"Applying {f.Name}",
(progress) => {
delta.ApplyDeltaPackageFast(workDir, f.FullName, progress);
progress(100);
return Task.CompletedTask;
});
}
await ctx.RunTask(
$"Building {Path.GetFileName(options.OutputFile)}",
async (progress) => {
await EasyZip.CreateZipFromDirectoryAsync(_logger, options.OutputFile, workDir, progress);
progress(100);
});
});
});
}
}
}

View File

@@ -7,4 +7,4 @@ public class DeltaPatchOptions
public FileInfo[] PatchFiles { get; set; }
public string OutputFile { get; set; }
}
}

View File

@@ -2,6 +2,7 @@
using System.Text;
using Microsoft.Extensions.Logging;
using Velopack.Compression;
using Velopack.Core;
using Velopack.Packaging.Exceptions;
using Velopack.Util;
@@ -26,11 +27,13 @@ public class DeltaPackageBuilder
public int Removed { get; set; }
}
public (ReleasePackage package, DeltaStats stats) CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile, DeltaMode mode, Action<int> progress)
public (ReleasePackage package, DeltaStats stats) CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile,
DeltaMode mode, Action<int> progress)
{
if (basePackage == null) throw new ArgumentNullException(nameof(basePackage));
if (newPackage == null) throw new ArgumentNullException(nameof(newPackage));
if (String.IsNullOrEmpty(outputFile) || File.Exists(outputFile)) throw new ArgumentException("The output file is null or already exists", nameof(outputFile));
if (String.IsNullOrEmpty(outputFile) || File.Exists(outputFile))
throw new ArgumentException("The output file is null or already exists", nameof(outputFile));
Zstd zstd = null;
try {
@@ -137,6 +140,7 @@ public class DeltaPackageBuilder
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
Interlocked.Increment(ref fChanged);
}
targetFile.Delete();
baseLibFiles.Remove(relativePath);
var p = Interlocked.Increment(ref fProcessed);
@@ -152,39 +156,47 @@ public class DeltaPackageBuilder
}
try {
Parallel.ForEach(newLibFiles, new ParallelOptions() { MaxDegreeOfParallelism = numParallel }, (f) => {
// we try to use zstd first, if it fails we'll try bsdiff
if (zstd != null) {
Parallel.ForEach(
newLibFiles,
new ParallelOptions() { MaxDegreeOfParallelism = numParallel },
(f) => {
// we try to use zstd first, if it fails we'll try bsdiff
if (zstd != null) {
try {
createDeltaForSingleFile(f, tempInfo, true);
return; // success, so return from this function
} catch (ProcessFailedException ex) {
_logger.Error(
$"Failed to create zstd diff for file '{f.FullName}' (will try to fallback to legacy bsdiff format - this will be much slower). " +
Environment.NewLine + ex.Message);
} catch (Exception ex) {
_logger.Error($"Failed to create zstd diff for file '{f.FullName}'. " + Environment.NewLine + ex.Message);
throw;
}
}
// if we're here, either zstd is not available or it failed
try {
createDeltaForSingleFile(f, tempInfo, true);
return; // success, so return from this function
} catch (ProcessFailedException ex) {
_logger.Error($"Failed to create zstd diff for file '{f.FullName}' (will try to fallback to legacy bsdiff format - this will be much slower). " + Environment.NewLine + ex.Message);
createDeltaForSingleFile(f, tempInfo, false);
if (zstd != null) {
_logger.Info($"Successfully created fallback bsdiff for file '{f.FullName}'.");
}
} catch (Exception ex) {
_logger.Error($"Failed to create zstd diff for file '{f.FullName}'. " + Environment.NewLine + ex.Message);
_logger.Error($"Failed to create bsdiff for file '{f.FullName}'. " + Environment.NewLine + ex.Message);
throw;
}
}
// if we're here, either zstd is not available or it failed
try {
createDeltaForSingleFile(f, tempInfo, false);
if (zstd != null) {
_logger.Info($"Successfully created fallback bsdiff for file '{f.FullName}'.");
}
} catch (Exception ex) {
_logger.Error($"Failed to create bsdiff for file '{f.FullName}'. " + Environment.NewLine + ex.Message);
throw;
}
});
});
} catch {
throw new UserInfoException("Delta creation failed for one or more files. See log for details. To skip delta generation, use the '--delta none' argument.");
throw new UserInfoException(
"Delta creation failed for one or more files. See log for details. To skip delta generation, use the '--delta none' argument.");
}
EasyZip.CreateZipFromDirectoryAsync(_logger, outputFile, tempInfo.FullName, CoreUtil.CreateProgressDelegate(progress, 70, 100)).GetAwaiterResult();
progress(100);
fRemoved = baseLibFiles.Count;
_logger.Info($"Delta processed {fProcessed:D4} files. "
_logger.Info(
$"Delta processed {fProcessed:D4} files. "
+ $"{fChanged:D4} patched, {fSame:D4} unchanged, {fNew:D4} new, {fRemoved:D4} removed");
_logger.Debug(
@@ -241,4 +253,4 @@ public class DeltaPackageBuilder
return true;
}
}
}

View File

@@ -9,7 +9,8 @@ public class ProcessFailedException : Exception
public string StdOutput { get; }
public ProcessFailedException(string command, string stdOutput, string stdErr)
: base($"Process failed: '{command}'{Environment.NewLine}Output was -{Environment.NewLine}{stdOutput}{Environment.NewLine}StdErr was -{Environment.NewLine}{stdErr}")
: base(
$"Process failed: '{command}'{Environment.NewLine}Output was -{Environment.NewLine}{stdOutput}{Environment.NewLine}StdErr was -{Environment.NewLine}{stdErr}")
{
Command = command;
StdOutput = stdOutput;
@@ -20,4 +21,4 @@ public class ProcessFailedException : Exception
if (result.ExitCode != 0)
throw new ProcessFailedException(result.Command, result.StdOutput, result.StdErr);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using Velopack.Core;
namespace Velopack.Packaging.Exceptions;
@@ -7,9 +8,9 @@ public class VelopackAppVerificationException : UserInfoException
{
public VelopackAppVerificationException(string message)
: base(
$"Failed to verify VelopackApp ({message}). " +
$"Ensure you have added the startup code to the beginning of your Program.Main(): VelopackApp.Build().Run(); " +
$"and then re-compile/re-publish your application.")
$"Failed to verify VelopackApp ({message}). " +
$"Ensure you have added the startup code to the beginning of your Program.Main(): VelopackApp.Build().Run(); " +
$"and then re-compile/re-publish your application.")
{
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text;
using Velopack.Core;
using Velopack.Packaging.Exceptions;
using Velopack.Util;
@@ -134,14 +135,15 @@ public static class Exe
process.Kill();
ct.ThrowIfCancellationRequested();
}
// need to call this once more to wait for the streams to finish. if WaitForExit is called with a timeout, the streams will not be fully read.
process.WaitForExit();
process.WaitForExit();
return (process.ExitCode, sOut.ToString().Trim(), sErr.ToString().Trim());
}
public static (int ExitCode, string StdOutput, string StdErr, string Command) InvokeProcess(string fileName, IEnumerable<string> args, string workingDirectory, CancellationToken ct = default,
public static (int ExitCode, string StdOutput, string StdErr, string Command) InvokeProcess(string fileName, IEnumerable<string> args,
string workingDirectory, CancellationToken ct = default,
IDictionary<string, string> envVar = null)
{
var psi = CreateProcessStartInfo(fileName, workingDirectory);

View File

@@ -1,9 +0,0 @@
namespace Velopack.Packaging.Flow;
#nullable enable
public class AuthConfiguration
{
public string? B2CAuthority { get; set; }
public string? RedirectUri { get; set; }
public string? ClientId { get; set; }
}

View File

@@ -1,9 +0,0 @@
#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

@@ -1,14 +0,0 @@
namespace Velopack.Packaging.Flow;
#nullable enable
public class Profile
{
public string? Id { get; set; }
public string? DisplayName { get; set; }
public string? Email { get; set; }
public string? GetDisplayName()
{
return DisplayName ?? Email ?? "<unknown>";
}
}

View File

@@ -1,20 +0,0 @@
#if NET6_0_OR_GREATER
#else
using System.Net.Http;
#endif
#nullable enable
namespace Velopack.Packaging.Flow;
internal sealed class FileUpload
{
public string? Status { get; set; }
public string? Md5Hash { get; set; }
}
internal sealed class ReleaseGroup
{
public Guid Id { get; set; }
public string? Version { get; set; }
public List<FileUpload>? FileUploads { get; set; }
}

View File

@@ -1,20 +0,0 @@
#nullable enable
using Newtonsoft.Json.Converters;
using Newtonsoft.Json;
namespace Velopack.Packaging.Flow;
internal sealed class UpdateReleaseGroupRequest
{
public string? NotesHtml { get; set; }
public string? NotesMarkdown { get; set; }
public ReleaseGroupState? State { get; set; }
}
[JsonConverter(typeof(StringEnumConverter))]
internal enum ReleaseGroupState
{
Draft,
Published,
Unlisted
}

View File

@@ -1,10 +0,0 @@
#nullable enable
namespace Velopack.Packaging.Flow;
public class UploadOptions(Stream releaseData, string fileName, string channel) : VelopackServiceOptions
{
public Stream ReleaseData { get; } = releaseData;
public string FileName { get; } = fileName;
public string Channel { get; } = channel;
}

View File

@@ -1,10 +0,0 @@
namespace Velopack.Packaging.Flow;
public class VelopackServiceOptions
{
public const string DefaultBaseUrl = "https://api.velopack.io/";
public string VelopackBaseUrl { get; set; } = DefaultBaseUrl;
public string ApiKey { get; set; } = string.Empty;
}

View File

@@ -5,9 +5,10 @@ using Markdig;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Compression;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.NuGet;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Exceptions;
using Velopack.Util;
namespace Velopack.Packaging;
@@ -44,8 +45,9 @@ public abstract class PackageBuilder<T> : ICommand<T>
options.TargetRuntime = RID.Parse(TargetOs.GetOsShortName());
}
if (options.TargetRuntime.BaseRID != TargetOs) {
throw new UserInfoException($"To build packages for {TargetOs.GetOsLongName()}, " +
if (options.TargetRuntime!.BaseRID != TargetOs) {
throw new UserInfoException(
$"To build packages for {TargetOs.GetOsLongName()}, " +
$"the target rid must be {TargetOs} (actually was {options.TargetRuntime?.BaseRID}). " +
$"If your real intention was to cross-compile a release for {options.TargetRuntime?.BaseRID} then you " +
$"should provide an OS directive: eg. 'vpk [{options.TargetRuntime?.BaseRID.GetOsShortName()}] pack ...'");
@@ -55,16 +57,19 @@ public abstract class PackageBuilder<T> : ICommand<T>
Log.Info("Releases Directory: " + options.ReleaseDir.FullName);
var releaseDir = options.ReleaseDir;
var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(TargetOs);
var channel = options.Channel?.ToLower() ?? DefaultName.GetDefaultChannel(TargetOs);
options.Channel = channel;
var entryHelper = new ReleaseEntryHelper(releaseDir.FullName, channel, Log, TargetOs);
if (entryHelper.DoesSimilarVersionExist(SemanticVersion.Parse(options.PackVersion))) {
if (await Console.PromptYesNo("A release in this channel with the same or greater version already exists. Do you want to continue and potentially overwrite files?") != true) {
throw new UserInfoException($"There is a release in channel {channel} which is equal or greater to the current version {options.PackVersion}. Please increase the current package version or remove that release.");
if (await Console.PromptYesNo(
"A release in this channel with the same or greater version already exists. Do you want to continue and potentially overwrite files?") !=
true) {
throw new UserInfoException(
$"There is a release in channel {channel} which is equal or greater to the current version {options.PackVersion}. Please increase the current package version or remove that release.");
}
}
using var _1 = TempUtil.GetTempDirectory(out var pkgTempDir);
TempDir = new DirectoryInfo(pkgTempDir);
@@ -85,10 +90,12 @@ public abstract class PackageBuilder<T> : ICommand<T>
break;
}
}
if (mainExePath == null) {
throw new UserInfoException(
$"Could not find main application executable (the one that runs 'VelopackApp.Build().Run()'). " + Environment.NewLine +
$"If your main binary is not named '{mainExeName}', please specify the name with the argument: --mainExe {{yourBinary.exe}}" + Environment.NewLine +
$"If your main binary is not named '{mainExeName}', please specify the name with the argument: --mainExe {{yourBinary.exe}}" +
Environment.NewLine +
$"I searched the following paths and none exist: " + Environment.NewLine +
String.Join(Environment.NewLine, mainSearchPaths)
);
@@ -105,72 +112,93 @@ public abstract class PackageBuilder<T> : ICommand<T>
var incomplete = Path.Combine(pkgTempDir, fileName);
var final = Path.Combine(releaseDir.FullName, fileName);
try { File.Delete(incomplete); } catch { }
filesToCopy.Add((incomplete, final));
return incomplete;
}
await Console.ExecuteProgressAsync(async (ctx) => {
ReleasePackage prev = null;
await ctx.RunTask("Pre-process steps", async (progress) => {
prev = entryHelper.GetPreviousFullRelease(NuGetVersion.Parse(packVersion));
packDirectory = await PreprocessPackDir(progress, packDirectory);
});
await Console.ExecuteProgressAsync(
async (ctx) => {
ReleasePackage prev = null;
await ctx.RunTask(
"Pre-process steps",
async (progress) => {
prev = entryHelper.GetPreviousFullRelease(NuGetVersion.Parse(packVersion));
packDirectory = await PreprocessPackDir(progress, packDirectory);
});
if (TargetOs != RuntimeOs.Linux) {
await ctx.RunTask("Code-sign application", async (progress) => {
await CodeSign(progress, packDirectory);
});
}
Task portableTask = null;
if (TargetOs == RuntimeOs.Linux || !Options.NoPortable) {
portableTask = ctx.RunTask("Building portable package", async (progress) => {
var suggestedName = ReleaseEntryHelper.GetSuggestedPortableName(packId, channel, TargetOs);
var path = getIncompletePath(suggestedName);
await CreatePortablePackage(progress, packDirectory, path);
});
}
// TODO: hack, this is a prerequisite for building full package but only on linux
if (TargetOs == RuntimeOs.Linux) await portableTask;
string releasePath = null;
await ctx.RunTask($"Building release {packVersion}", async (progress) => {
var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, false, TargetOs);
releasePath = getIncompletePath(suggestedName);
await CreateReleasePackage(progress, packDirectory, releasePath);
});
Task setupTask = null;
if (!Options.NoInst && TargetOs != RuntimeOs.Linux) {
setupTask = ctx.RunTask("Building setup package", async (progress) => {
var suggestedName = ReleaseEntryHelper.GetSuggestedSetupName(packId, channel, TargetOs);
var path = getIncompletePath(suggestedName);
await CreateSetupPackage(progress, releasePath, packDirectory, path);
});
}
if (prev != null && options.DeltaMode != DeltaMode.None) {
await ctx.RunTask($"Building delta {prev.Version} -> {packVersion}", async (progress) => {
var suggestedName = ReleaseEntryHelper.GetSuggestedReleaseName(packId, packVersion, channel, true, TargetOs);
var deltaPkg = await CreateDeltaPackage(progress, releasePath, prev.PackageFile, getIncompletePath(suggestedName), options.DeltaMode);
});
}
if (TargetOs != RuntimeOs.Linux && portableTask != null) await portableTask;
if (setupTask != null) await setupTask;
await ctx.RunTask("Post-process steps", (progress) => {
foreach (var f in filesToCopy) {
IoUtil.MoveFile(f.from, f.to, true);
if (TargetOs != RuntimeOs.Linux) {
await ctx.RunTask(
"Code-sign application",
async (progress) => {
await CodeSign(progress, packDirectory);
});
}
ReleaseEntryHelper.UpdateReleaseFiles(releaseDir.FullName, Log);
BuildAssets.Write(releaseDir.FullName, channel, filesToCopy.Select(x => x.to));
progress(100);
return Task.CompletedTask;
Task portableTask = null;
if (TargetOs == RuntimeOs.Linux || !Options.NoPortable) {
portableTask = ctx.RunTask(
"Building portable package",
async (progress) => {
var suggestedName = DefaultName.GetSuggestedPortableName(packId, channel, TargetOs);
var path = getIncompletePath(suggestedName);
await CreatePortablePackage(progress, packDirectory, path);
});
}
// TODO: hack, this is a prerequisite for building full package but only on linux
if (TargetOs == RuntimeOs.Linux) await portableTask!;
string releasePath = null;
await ctx.RunTask(
$"Building release {packVersion}",
async (progress) => {
var suggestedName = DefaultName.GetSuggestedReleaseName(packId, packVersion, channel, false, TargetOs);
releasePath = getIncompletePath(suggestedName);
await CreateReleasePackage(progress, packDirectory, releasePath);
});
Task setupTask = null;
if (!Options.NoInst && TargetOs != RuntimeOs.Linux) {
setupTask = ctx.RunTask(
"Building setup package",
async (progress) => {
var suggestedName = DefaultName.GetSuggestedSetupName(packId, channel, TargetOs);
var path = getIncompletePath(suggestedName);
await CreateSetupPackage(progress, releasePath, packDirectory, path);
});
}
if (prev != null && options.DeltaMode != DeltaMode.None) {
await ctx.RunTask(
$"Building delta {prev.Version} -> {packVersion}",
async (progress) => {
var suggestedName = DefaultName.GetSuggestedReleaseName(packId, packVersion, channel, true, TargetOs);
var deltaPkg = await CreateDeltaPackage(
progress,
releasePath,
prev.PackageFile,
getIncompletePath(suggestedName),
options.DeltaMode);
});
}
if (TargetOs != RuntimeOs.Linux && portableTask != null) await portableTask;
if (setupTask != null) await setupTask;
await ctx.RunTask(
"Post-process steps",
(progress) => {
foreach (var f in filesToCopy) {
IoUtil.MoveFile(f.from, f.to, true);
}
ReleaseEntryHelper.UpdateReleaseFiles(releaseDir.FullName, Log);
BuildAssets.Write(releaseDir.FullName, channel, filesToCopy.Select(x => x.to));
progress(100);
return Task.CompletedTask;
});
});
});
}
protected virtual string ExtractPackDir(string packDirectory) => packDirectory;
@@ -187,12 +215,14 @@ public abstract class PackageBuilder<T> : ICommand<T>
var rid = Options.TargetRuntime;
string extraMetadata = "";
void addMetadata(string key, string value)
{
if (!String.IsNullOrEmpty(key) && !String.IsNullOrEmpty(value)) {
if (!SecurityElement.IsValidText(value)) {
value = $"""<![CDATA[{"\n"}{value}{"\n"}]]>""";
}
extraMetadata += $"<{key}>{value}</{key}>{Environment.NewLine}";
}
}
@@ -218,22 +248,22 @@ public abstract class PackageBuilder<T> : ICommand<T>
}
string nuspec = $"""
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>{packId}</id>
<title>{packTitle ?? packId}</title>
<description>{packTitle ?? packId}</description>
<authors>{packAuthors ?? packId}</authors>
<version>{packVersion}</version>
<channel>{Options.Channel}</channel>
<mainExe>{Options.EntryExecutableName}</mainExe>
<os>{rid.BaseRID.GetOsShortName()}</os>
<rid>{rid.ToDisplay(RidDisplayType.NoVersion)}</rid>
{extraMetadata.Trim()}
</metadata>
</package>
""".Trim();
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<id>{packId}</id>
<title>{packTitle ?? packId}</title>
<description>{packTitle ?? packId}</description>
<authors>{packAuthors ?? packId}</authors>
<version>{packVersion}</version>
<channel>{Options.Channel}</channel>
<mainExe>{Options.EntryExecutableName}</mainExe>
<os>{rid.BaseRID.GetOsShortName()}</os>
<rid>{rid.ToDisplay(RidDisplayType.NoVersion)}</rid>
{extraMetadata.Trim()}
</metadata>
</package>
""".Trim();
return nuspec;
}
@@ -315,6 +345,7 @@ public abstract class PackageBuilder<T> : ICommand<T>
Log.Debug("Skipping because matched exclude pattern: " + path);
continue;
}
fileInfo.CopyTo(path, true);
}
@@ -356,12 +387,12 @@ public abstract class PackageBuilder<T> : ICommand<T>
.ToArray();
var contentType = $"""
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
{String.Join(Environment.NewLine, extensions)}
</Types>
""";
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
{String.Join(Environment.NewLine, extensions)}
</Types>
""";
File.WriteAllText(Path.Combine(rootDirectory, NugetUtil.ContentTypeFileName), contentType);
@@ -369,11 +400,11 @@ public abstract class PackageBuilder<T> : ICommand<T>
Directory.CreateDirectory(relsDir);
var rels = $"""
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.microsoft.com/packaging/2010/07/manifest" Target="/{Path.GetFileName(nuspecPath)}" Id="R1" />
</Relationships>
""";
<?xml version="1.0" encoding="utf-8"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Type="http://schemas.microsoft.com/packaging/2010/07/manifest" Target="/{Path.GetFileName(nuspecPath)}" Id="R1" />
</Relationships>
""";
File.WriteAllText(Path.Combine(relsDir, ".rels"), rels);
}
}
}

View File

@@ -1,6 +1,7 @@
using System.Text;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Core;
using Velopack.NuGet;
using Velopack.Util;
@@ -17,7 +18,7 @@ public class ReleaseEntryHelper
{
_outputDir = outputDir;
_logger = logger;
_channel = channel ?? GetDefaultChannel(os);
_channel = channel ?? DefaultName.GetDefaultChannel(os);
_releases = GetReleasesFromDir(outputDir);
}
@@ -26,11 +27,12 @@ public class ReleaseEntryHelper
var rel = new Dictionary<string, List<VelopackAsset>>(StringComparer.OrdinalIgnoreCase);
foreach (var releaseFile in Directory.EnumerateFiles(dir, "*.nupkg")) {
var zip = new ZipPackage(releaseFile);
var ch = zip.Channel ?? GetDefaultChannel(VelopackRuntimeInfo.SystemOs);
var ch = zip.Channel ?? DefaultName.GetDefaultChannel(VelopackRuntimeInfo.SystemOs);
if (!rel.ContainsKey(ch))
rel[ch] = new List<VelopackAsset>();
rel[ch].Add(VelopackAsset.FromZipPackage(zip));
}
return rel;
}
@@ -43,6 +45,7 @@ public class ReleaseEntryHelper
return true;
}
}
return false;
}
@@ -94,10 +97,12 @@ public class ReleaseEntryHelper
foreach (var releaseFile in Directory.EnumerateFiles(outputDir, "RELEASES*")) {
File.Delete(releaseFile);
}
foreach (var kvp in releases) {
var exclude = kvp.Value.Where(x => x.Version.ReleaseLabels.Any(r => r.Contains('.')) || x.Version.HasMetadata).ToArray();
if (exclude.Any()) {
log.Warn($"Excluding {exclude.Length} asset(s) from legacy RELEASES file, because they " +
log.Warn(
$"Excluding {exclude.Length} asset(s) from legacy RELEASES file, because they " +
$"contain an invalid character in the version: {string.Join(", ", exclude.Select(x => x.FileName))}");
}
@@ -151,54 +156,6 @@ public class ReleaseEntryHelper
return Encoding.UTF8.GetString(ms.ToArray());
}
public static string GetSuggestedReleaseName(string id, string version, string channel, bool delta, RuntimeOs os)
{
var suffix = GetUniqueAssetSuffix(channel);
version = SemanticVersion.Parse(version).ToNormalizedString();
if (os == RuntimeOs.Windows && channel == GetDefaultChannel(RuntimeOs.Windows)) {
return $"{id}-{version}{(delta ? "-delta" : "-full")}.nupkg";
}
return $"{id}-{version}{suffix}{(delta ? "-delta" : "-full")}.nupkg";
}
public static string GetSuggestedPortableName(string id, string channel, RuntimeOs os)
{
var suffix = GetUniqueAssetSuffix(channel);
if (os == RuntimeOs.Linux) {
if (channel == GetDefaultChannel(RuntimeOs.Linux)) {
return $"{id}.AppImage";
} else {
return $"{id}{suffix}.AppImage";
}
} else {
return $"{id}{suffix}-Portable.zip";
}
}
public static string GetSuggestedSetupName(string id, string channel, RuntimeOs os)
{
var suffix = GetUniqueAssetSuffix(channel);
if (os == RuntimeOs.Windows)
return $"{id}{suffix}-Setup.exe";
else if (os == RuntimeOs.OSX)
return $"{id}{suffix}-Setup.pkg";
else
throw new PlatformNotSupportedException("Platform not supported.");
}
private static string GetUniqueAssetSuffix(string channel)
{
return "-" + channel;
}
public static string GetDefaultChannel(RuntimeOs os)
{
if (os == RuntimeOs.Windows) return "win";
if (os == RuntimeOs.OSX) return "osx";
if (os == RuntimeOs.Linux) return "linux";
throw new NotSupportedException("Unsupported OS: " + os);
}
public enum AssetsMode
{
AllPackages,
@@ -267,6 +224,4 @@ public class ReleaseEntryHelper
// return ret;
//}
}
}

View File

@@ -16,4 +16,4 @@ public class ReleasePackage
public string PackageFile { get; protected set; }
public SemanticVersion Version => _package.Value.Version;
}
}

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\..\lib-csharp\Velopack.csproj" />
<ProjectReference Include="..\Velopack.Core\Velopack.Core.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging.Exceptions;
using Velopack.Core;
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging;
@@ -30,7 +31,8 @@ public class Zstd
}
if (windowLog > 30) {
throw new UserInfoException($"The file '{Path.GetFileName(oldFile)}' is too large for delta compression. You can disable delta generation using '--delta none'.");
throw new UserInfoException(
$"The file '{Path.GetFileName(oldFile)}' is too large for delta compression. You can disable delta generation using '--delta none'.");
}
if (mode == DeltaMode.BestSize) {
@@ -64,7 +66,11 @@ public class Zstd
{
int count = 0;
v >>= 1;
while (v > 0) { v >>= 1; count++; }
while (v > 0) {
v >>= 1;
count++;
}
return count;
}
}
}

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging;
using Velopack.Core;
using Velopack.Packaging;
namespace Velopack.Vpk.Commands;

View File

@@ -1,26 +0,0 @@
using System.Threading;
using Serilog.Core;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public class ApiCommandRunner(IVelopackFlowServiceClient Client) : ICommand<ApiOptions>
{
public async Task Run(ApiOptions options)
{
CancellationToken token = CancellationToken.None;
if (!await Client.LoginAsync(new VelopackLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl
}, true, token)) {
return;
}
string response = await Client.InvokeEndpointAsync(options, options.Endpoint, options.Method, options.Body, token);
Console.WriteLine(response);
}
}

View File

@@ -1,11 +0,0 @@
#nullable enable
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public sealed class ApiOptions : VelopackServiceOptions
{
public string Endpoint { get; set; } = "";
public string Method { get; set; } = "";
public string? Body { get; set; }
}

View File

@@ -1,17 +0,0 @@
using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
#nullable enable
public class LoginCommandRunner(IVelopackFlowServiceClient Client) : ICommand<LoginOptions>
{
public async Task Run(LoginOptions options)
{
await Client.LoginAsync(new() {
VelopackBaseUrl = options.VelopackBaseUrl,
ApiKey = options.ApiKey,
}, false, CancellationToken.None);
}
}

View File

@@ -1,5 +0,0 @@
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public sealed class LoginOptions : VelopackServiceOptions;

View File

@@ -1,14 +0,0 @@
using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
#nullable enable
namespace Velopack.Vpk.Commands.Flow;
internal class LogoutCommandRunner(IVelopackFlowServiceClient Client) : ICommand<LogoutOptions>
{
public async Task Run(LogoutOptions options)
{
await Client.LogoutAsync(options, CancellationToken.None);
}
}

View File

@@ -1,5 +0,0 @@
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public sealed class LogoutOptions : VelopackServiceOptions;

View File

@@ -7,7 +7,7 @@ public class PublishCommand : VelopackServiceCommand
public string? Channel { get; set; }
public bool NoWaitForLive { get; set; }
public bool WaitForLive { get; set; }
public PublishCommand()
: base("publish", "Uploads a release to Velopack's hosted service")
@@ -21,7 +21,7 @@ public class PublishCommand : VelopackServiceCommand
.SetArgumentHelpName("NAME")
.SetDescription("The channel used for the release.");
AddOption<bool>(v => NoWaitForLive = v, "--noWaitForLive")
.SetDescription("Skip waiting for the release to finish processing and go live.");
AddOption<bool>(v => WaitForLive = v, "--waitForLive")
.SetDescription("Wait for the release to finish processing and go live.");
}
}

View File

@@ -1,25 +0,0 @@
using System.Threading;
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand<PublishOptions>
{
public async Task Run(PublishOptions options)
{
CancellationToken token = CancellationToken.None;
if (!await Client.LoginAsync(new VelopackLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl
}, false, token)) {
return;
}
await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory,
options.VelopackBaseUrl, options.TargetOs, options.NoWaitForLive, token);
}
}

View File

@@ -1,15 +0,0 @@
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
#nullable enable
public sealed class PublishOptions : VelopackServiceOptions
{
public RuntimeOs TargetOs { get; set; }
public string ReleaseDirectory { get; set; } = "";
public string? Channel { get; set; }
public bool NoWaitForLive { get; set; }
}

View File

@@ -1,23 +1,27 @@
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
namespace Velopack.Vpk.Commands.Flow;
public abstract class VelopackServiceCommand : BaseCommand
{
public string VelopackBaseUrl { get; private set; }
public string ApiKey { get; private set; }
public double Timeout { get; private set; }
protected VelopackServiceCommand(string name, string description)
: base(name, description)
{
AddOption<string>(v => VelopackBaseUrl = v, "--baseUrl")
.SetDescription("The base Uri for the Velopack API service.")
.SetArgumentHelpName("URI")
.SetDefault(VelopackServiceOptions.DefaultBaseUrl);
.SetArgumentHelpName("URI");
AddOption<string>(v => ApiKey = v, "--api-key")
.SetDescription("The API key to use to authenticate with Velopack API service.")
.SetArgumentHelpName("ApiKey");
AddOption<double>((v) => Timeout = v, "--timeout")
.SetDescription("Network timeout in minutes.")
.SetArgumentHelpName("MINUTES")
.SetDefault(30);
}
}

View File

@@ -1,4 +1,5 @@
using Velopack.Packaging;
using Velopack.Core;
using Velopack.Packaging;
namespace Velopack.Vpk.Commands;
@@ -24,7 +25,7 @@ public abstract class OutputCommand : BaseCommand
.SetDescription("The channel to use for this release.")
.RequiresValidNuGetId()
.SetArgumentHelpName("NAME")
.SetDefault(ReleaseEntryHelper.GetDefaultChannel(targetOs == RuntimeOs.Unknown ? VelopackRuntimeInfo.SystemOs : targetOs));
.SetDefault(DefaultName.GetDefaultChannel(targetOs == RuntimeOs.Unknown ? VelopackRuntimeInfo.SystemOs : targetOs));
}
public DirectoryInfo GetReleaseDirectory()

View File

@@ -1,4 +1,4 @@
using Velopack.Packaging.Abstractions;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Vpk.Logging;

View File

@@ -1,6 +1,6 @@
using System.Threading;
using Spectre.Console;
using Velopack.Packaging.Abstractions;
using Velopack.Core.Abstractions;
using Velopack.Util;
namespace Velopack.Vpk.Logging;

View File

@@ -1,5 +1,6 @@
using Riok.Mapperly.Abstractions;
using Velopack.Deployment;
using Velopack.Flow.Commands;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;

View File

@@ -5,11 +5,13 @@ using Microsoft.Extensions.Hosting;
using Serilog;
using Serilog.Core;
using Serilog.Events;
using Velopack.Core;
using Velopack.Core.Abstractions;
using Velopack.Deployment;
using Velopack.Packaging.Abstractions;
using Velopack.Flow;
using Velopack.Flow.Commands;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Flow;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;
using Velopack.Util;
@@ -94,8 +96,7 @@ public class Program
SetupConfig(builder);
SetupLogging(builder, verbose, legacyConsole);
SetupVelopackService(builder.Services);
RuntimeOs targetOs = VelopackRuntimeInfo.SystemOs;
if (new bool[] { directiveWin, directiveLinux, directiveOsx }.Count(x => x) > 1) {
throw new UserInfoException(
@@ -208,15 +209,6 @@ public class Program
Log.Logger = conf.CreateLogger();
builder.Logging.AddSerilog();
}
private static void SetupVelopackService(IServiceCollection services)
{
services.AddSingleton<IVelopackFlowServiceClient, VelopackFlowServiceClient>();
services.AddSingleton<HmacAuthHttpClientHandler>();
services.AddHttpClient().ConfigureHttpClientDefaults(x =>
x.AddHttpMessageHandler<HmacAuthHttpClientHandler>()
.ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromMinutes(60)));
}
}
public static class ProgramCommandExtensions

View File

@@ -33,6 +33,7 @@
<ItemGroup>
<ProjectReference Include="..\Velopack.Deployment\Velopack.Deployment.csproj" />
<ProjectReference Include="..\Velopack.Flow\Velopack.Flow.csproj" />
<ProjectReference Include="..\Velopack.Packaging.Unix\Velopack.Packaging.Unix.csproj" />
<ProjectReference Include="..\Velopack.Packaging.Windows\Velopack.Packaging.Windows.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using Neovolve.Logging.Xunit;
using Velopack.Core;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Windows;
using Velopack.Util;

View File

@@ -1,6 +1,7 @@
using Velopack.Deployment;
using Velopack.Sources;
using Octokit;
using Velopack.Core;
using Velopack.Packaging.Exceptions;
using Velopack.Util;

View File

@@ -5,6 +5,7 @@ using System.Xml.Linq;
using Microsoft.Win32;
using NuGet.Packaging;
using Velopack.Compression;
using Velopack.Core;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Exceptions;
using Velopack.Packaging.Windows.Commands;

View File

@@ -1,7 +1,7 @@
#pragma warning disable CS0618 // Type or member is obsolete
#pragma warning disable CS0612 // Type or member is obsolete
using System.Text;
using Velopack.Packaging;
using Velopack.Core;
using Velopack.Sources;
using Velopack.Util;
@@ -39,13 +39,14 @@ internal class FakeFixtureRepository : Sources.IFileDownloader
var name = new ReleaseEntryName(maxfullVer.PackageId, maxDeltaVer.Version, false);
releases.Add(new ReleaseEntry("0000000000000000000000000000000000000000", name.ToFileName(), maxfullVer.Filesize));
releasesNew.Add(new VelopackAsset {
PackageId = maxfullVer.PackageId,
Version = maxDeltaVer.Version,
Type = VelopackAssetType.Full,
FileName = $"{maxfullVer.PackageId}-{maxDeltaVer.Version}-full.nupkg",
Size = maxfullVer.Filesize,
});
releasesNew.Add(
new VelopackAsset {
PackageId = maxfullVer.PackageId,
Version = maxDeltaVer.Version,
Type = VelopackAssetType.Full,
FileName = $"{maxfullVer.PackageId}-{maxDeltaVer.Version}-full.nupkg",
Size = maxfullVer.Filesize,
});
}
}
@@ -80,7 +81,8 @@ internal class FakeFixtureRepository : Sources.IFileDownloader
return Task.FromResult(File.ReadAllBytes(filePath));
}
public Task DownloadFile(string url, string targetFile, Action<int> progress, string authorization = null, string accept = null, double timeout = 30, CancellationToken token = default)
public Task DownloadFile(string url, string targetFile, Action<int> progress, string authorization = null, string accept = null, double timeout = 30,
CancellationToken token = default)
{
var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename));
var filePath = PathHelper.GetFixture(rel.OriginalFilename);
@@ -111,4 +113,4 @@ internal class FakeFixtureRepository : Sources.IFileDownloader
throw new NotSupportedException("FakeFixtureRepository doesn't have: " + url);
}
}
}

View File

@@ -1,7 +1,7 @@
using System.Text;
using NuGet.Versioning;
using Velopack.Compression;
using Velopack.Packaging;
using Velopack.Core;
using Velopack.Locators;
using Velopack.Sources;
using Velopack.Tests.TestHelpers;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);CA1416</NoWarn>
</PropertyGroup>
@@ -19,29 +19,29 @@
</Choose>
<ItemGroup>
<Compile Include="..\..\src\vpk\Velopack.Packaging\SimpleJson.cs" Link="SimpleJson.cs" />
<Compile Include="..\..\src\vpk\Velopack.Core\SimpleJson.cs" Link="SimpleJson.cs"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging" Version="9.0.0" />
<PackageReference Include="System.IO.Packaging" Version="9.0.0"/>
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">
<Reference Include="System.Web" />
<Reference Include="System.Net.Http" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
<Reference Include="System.Web"/>
<Reference Include="System.Net.Http"/>
<Reference Include="System.IO.Compression"/>
<Reference Include="System.IO.Compression.FileSystem"/>
</ItemGroup>
<Choose>
<When Condition="'$(TargetFramework)' == 'net6.0'">
<ItemGroup>
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj" SetTargetFramework="TargetFramework=netstandard2.0" />
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj" SetTargetFramework="TargetFramework=netstandard2.0"/>
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj" />
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj"/>
</ItemGroup>
</Otherwise>
</Choose>