Refactor downloader methods to use a dictionary for headers, has effect on Sources

This commit is contained in:
Thomas Haid
2025-05-15 21:07:45 +02:00
committed by Caelan
parent dd53fb3bc9
commit fec5b97a41
8 changed files with 77 additions and 51 deletions

View File

@@ -35,12 +35,14 @@ namespace Velopack.Sources
protected virtual string? AccessToken { get; }
/// <summary>
/// The Bearer token used in the request.
/// The Bearer or other type of Authorization header used to authenticate against the Api.
/// </summary>
protected virtual string? Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "Bearer " + AccessToken;
protected abstract (string Name, string Value) Authorization { get; }
/// <inheritdoc />
public GitBase(string repoUrl, string? accessToken, bool prerelease, IFileDownloader? downloader = null)
/// <summary>
/// Base constructor.
/// </summary>
protected GitBase(string repoUrl, string? accessToken, bool prerelease, IFileDownloader? downloader = null)
{
RepoUri = new Uri(repoUrl.TrimEnd('/'));
AccessToken = accessToken;
@@ -55,7 +57,12 @@ namespace Velopack.Sources
// this might be a browser url or an api url (depending on whether we have a AccessToken or not)
// https://docs.github.com/en/rest/reference/releases#get-a-release-asset
var assetUrl = GetAssetUrlFromName(githubEntry.Release, releaseEntry.FileName);
return Downloader.DownloadFile(assetUrl, localFile, progress, Authorization, "application/octet-stream", cancelToken: cancelToken);
return Downloader.DownloadFile(assetUrl, localFile, progress,
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/octet-stream"
},
cancelToken: cancelToken);
}
throw new ArgumentException($"Expected releaseEntry to be {nameof(GitBaseAsset)} but got {releaseEntry.GetType().Name}.");
@@ -84,7 +91,12 @@ namespace Velopack.Sources
logger.Trace(ex.ToString());
continue;
}
var releaseBytes = await Downloader.DownloadBytes(assetUrl, Authorization, "application/octet-stream").ConfigureAwait(false);
var releaseBytes = await Downloader.DownloadBytes(assetUrl,
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/octet-stream"
}
).ConfigureAwait(false);
var txt = CoreUtil.RemoveByteOrderMarkerIfPresent(releaseBytes);
var feed = VelopackAssetFeed.FromJson(txt);
foreach (var f in feed.Assets) {

View File

@@ -81,11 +81,10 @@ namespace Velopack.Sources
: base(repoUrl, accessToken, prerelease, downloader)
{
}
/// <summary>
/// The authorization token used in the request.
/// Overwrite it to token
/// </summary>
protected override string? Authorization => string.IsNullOrWhiteSpace(AccessToken) ? null : "token " + AccessToken;
/// <inheritdoc cref="Authorization"/>
protected override (string Name, string Value) Authorization => ("Authorization", $"token {AccessToken}");
/// <inheritdoc />
protected override async Task<GiteaRelease[]> GetReleases(bool includePrereleases)
{
@@ -97,7 +96,12 @@ namespace Velopack.Sources
var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?limit={perPage}&page={page}&draft=false";
var baseUri = GetApiBaseUrl(RepoUri);
var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization, "application/json").ConfigureAwait(false);
var response = await Downloader.DownloadString(getReleasesUri.ToString(),
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/json"
}
).ConfigureAwait(false);
var releases = CompiledJson.DeserializeGiteaReleaseList(response);
if (releases == null) return new GiteaRelease[0];
return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray();

View File

@@ -87,6 +87,9 @@ namespace Velopack.Sources
{
}
/// <inheritdoc cref="Authorization"/>
protected override (string Name, string Value) Authorization => ("Authorization", $"Bearer {AccessToken}");
/// <inheritdoc />
protected override async Task<GithubRelease[]> GetReleases(bool includePrereleases)
{
@@ -96,7 +99,12 @@ namespace Velopack.Sources
var releasesPath = $"repos{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}";
var baseUri = GetApiBaseUrl(RepoUri);
var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization, "application/vnd.github.v3+json").ConfigureAwait(false);
var response = await Downloader.DownloadString(getReleasesUri.ToString(),
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/vnd.github.v3+json"
}
).ConfigureAwait(false);
var releases = CompiledJson.DeserializeGithubReleaseList(response);
if (releases == null) return Array.Empty<GithubRelease>();
return releases.OrderByDescending(d => d.PublishedAt).Where(x => includePrereleases || !x.Prerelease).ToArray();

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Velopack.Util;
@@ -98,6 +99,9 @@ namespace Velopack.Sources
/// </summary>
public class GitlabSource : GitBase<GitlabRelease>
{
/// <inheritdoc cref="Authorization"/>
protected override (string Name, string Value) Authorization => ("PRIVATE-TOKEN", AccessToken ?? string.Empty);
/// <inheritdoc cref="GitlabSource" />
/// <param name="repoUrl">
/// The URL of the GitLab repository to download releases from
@@ -156,7 +160,11 @@ namespace Velopack.Sources
var releasesPath = $"{RepoUri.AbsolutePath}/releases?per_page={perPage}&page={page}";
var baseUri = new Uri("https://gitlab.com");
var getReleasesUri = new Uri(baseUri, releasesPath);
var response = await Downloader.DownloadString(getReleasesUri.ToString(), Authorization).ConfigureAwait(false);
var response = await Downloader.DownloadString(getReleasesUri.ToString(),
new Dictionary<string, string> {
[Authorization.Name] = Authorization.Value,
["Accept"] = "application/json"
}).ConfigureAwait(false);
var releases = CompiledJson.DeserializeGitlabReleaseList(response);
if (releases == null) return new GitlabRelease[0];
return releases.OrderByDescending(d => d.ReleasedAt).Where(x => includePrereleases || !x.UpcomingRelease).ToArray();

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
@@ -18,9 +19,9 @@ namespace Velopack.Sources
public static ProductInfoHeaderValue UserAgent => new("Velopack", VelopackRuntimeInfo.VelopackNugetVersion.ToFullString());
/// <inheritdoc />
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, string? authorization, string? accept, double timeout, CancellationToken cancelToken = default)
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, IDictionary<string, string>? headers, double timeout, CancellationToken cancelToken = default)
{
using var client = CreateHttpClient(authorization, accept, timeout);
using var client = CreateHttpClient(headers, timeout);
try {
using (var fs = File.Open(targetFile, FileMode.Create)) {
@@ -36,9 +37,9 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<byte[]> DownloadBytes(string url, string? authorization, string? accept, double timeout)
public virtual async Task<byte[]> DownloadBytes(string url, IDictionary<string, string>? headers, double timeout)
{
using var client = CreateHttpClient(authorization, accept, timeout);
using var client = CreateHttpClient(headers, timeout);
try {
return await client.GetByteArrayAsync(url).ConfigureAwait(false);
@@ -50,9 +51,9 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<string> DownloadString(string url, string? authorization, string? accept, double timeout)
public virtual async Task<string> DownloadString(string url, IDictionary<string, string>? headers, double timeout)
{
using var client = CreateHttpClient(authorization, accept, timeout);
using var client = CreateHttpClient(headers, timeout);
try {
return await client.GetStringAsync(url).ConfigureAwait(false);
@@ -123,16 +124,15 @@ namespace Velopack.Sources
/// <summary>
/// Creates a new <see cref="HttpClient"/> for every request.
/// </summary>
protected virtual HttpClient CreateHttpClient(string? authorization, string? accept, double timeout = 30)
protected virtual HttpClient CreateHttpClient(IDictionary<string, string>? headers, double timeout)
{
var client = new HttpClient(CreateHttpClientHandler());
client.DefaultRequestHeaders.UserAgent.Add(UserAgent);
if (authorization != null)
client.DefaultRequestHeaders.Add("Authorization", authorization);
if (accept != null)
client.DefaultRequestHeaders.Add("Accept", accept);
foreach (var header in headers ?? new Dictionary<string, string>())
{
client.DefaultRequestHeaders.Add(header.Key, header.Value);
}
client.Timeout = TimeSpan.FromMinutes(timeout);
return client;

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -19,26 +20,21 @@ namespace Velopack.Sources
/// <param name="progress">
/// A delegate for reporting download progress, with expected values from 0-100.
/// </param>
/// <param name="authorization">
/// Text to be sent in the 'Authorization' header of the request.
/// </param>
/// <param name="accept">
/// Text to be sent in the 'Accept' header of the request.
/// </param>
/// <param name="headers">Headers that can be passed to Http Downloader, e.g. Accept or Authorization.</param>
/// <param name="timeout">
/// The maximum time in minutes to wait for the download to complete.
/// </param>
/// <param name="cancelToken">Optional token to cancel the request.</param>
Task DownloadFile(string url, string targetFile, Action<int> progress, string? authorization = null, string? accept = null, double timeout = 30, CancellationToken cancelToken = default);
Task DownloadFile(string url, string targetFile, Action<int> progress, IDictionary<string, string>? headers = null, double timeout = 30, CancellationToken cancelToken = default);
/// <summary>
/// Returns a byte array containing the contents of the file at the specified url
/// </summary>
Task<byte[]> DownloadBytes(string url, string? authorization = null, string? accept = null, double timeout = 30);
Task<byte[]> DownloadBytes(string url, IDictionary<string, string>? headers = null, double timeout = 30);
/// <summary>
/// Returns a string containing the contents of the specified url
/// </summary>
Task<string> DownloadString(string url, string? authorization = null, string? accept = null, double timeout = 30);
Task<string> DownloadString(string url, IDictionary<string, string>? headers = null, double timeout = 30);
}
}

View File

@@ -7,23 +7,21 @@ public class FakeDownloader : IFileDownloader
{
public string LastUrl { get; private set; }
public string LastLocalFile { get; private set; }
public string LastAuthHeader { get; private set; }
public string LastAcceptHeader { get; private set; }
public IDictionary<string, string>? LastHeaders { get; private set; }
public byte[] MockedResponseBytes { get; set; } = [];
public bool WriteMockLocalFile { get; set; } = false;
public Task<byte[]> DownloadBytes(string url, string auth, string acc, double timeout = 30)
public Task<byte[]> DownloadBytes(string url, IDictionary<string, string> headers, double timeout = 30)
{
LastUrl = url;
LastAuthHeader = auth;
LastAcceptHeader = acc;
LastHeaders = headers;
return Task.FromResult(MockedResponseBytes);
}
public async Task DownloadFile(string url, string targetFile, Action<int> progress, string auth, string acc, double timeout, CancellationToken token)
public async Task DownloadFile(string url, string targetFile, Action<int> progress, IDictionary<string, string> headers, double timeout = 30, CancellationToken token = default)
{
LastLocalFile = targetFile;
var resp = await DownloadBytes(url, auth, acc);
var resp = await DownloadBytes(url, headers);
progress?.Invoke(25);
progress?.Invoke(50);
progress?.Invoke(75);
@@ -32,8 +30,8 @@ public class FakeDownloader : IFileDownloader
File.WriteAllBytes(targetFile, resp);
}
public async Task<string> DownloadString(string url, string auth, string acc, double timeout = 30)
public async Task<string> DownloadString(string url, IDictionary<string, string> headers, double timeout = 30)
{
return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc));
return Encoding.UTF8.GetString(await DownloadBytes(url, headers));
}
}

View File

@@ -57,7 +57,7 @@ internal class FakeFixtureRepository : IFileDownloader
_releases = releases;
}
public Task<byte[]> DownloadBytes(string url, string authorization = null, string accept = null, double timeout = 30)
public Task<byte[]> DownloadBytes(string url, IDictionary<string, string> headers = null, double timeout = 30)
{
if (url.Contains($"/{_releasesName}?")) {
MemoryStream ms = new MemoryStream();
@@ -82,7 +82,7 @@ internal class FakeFixtureRepository : 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,
public Task DownloadFile(string url, string targetFile, Action<int> progress, IDictionary<string, string> headers = null, double timeout = 30,
CancellationToken token = default)
{
var rel = _releases.FirstOrDefault(r => url.EndsWith(r.OriginalFilename));
@@ -99,7 +99,7 @@ internal class FakeFixtureRepository : IFileDownloader
return Task.CompletedTask;
}
public Task<string> DownloadString(string url, string authorization = null, string accept = null, double timeout = 30)
public Task<string> DownloadString(string url, IDictionary<string, string> headers = null, double timeout = 30)
{
if (url.Contains($"/{_releasesName}?")) {
MemoryStream ms = new MemoryStream();