Add --timeout as a cli option

This commit is contained in:
Ben Evans
2024-11-28 23:34:48 +00:00
committed by Caelan
parent c03a960a25
commit ea0a6ce8b7
17 changed files with 92 additions and 28 deletions

View File

@@ -55,7 +55,7 @@ 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);
return Downloader.DownloadFile(assetUrl, localFile, progress, Authorization, "application/octet-stream", cancelToken: cancelToken);
}
throw new ArgumentException($"Expected releaseEntry to be {nameof(GitBaseAsset)} but got {releaseEntry.GetType().Name}.");

View File

@@ -18,9 +18,10 @@ 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, CancellationToken cancelToken = default)
public virtual async Task DownloadFile(string url, string targetFile, Action<int> progress, string? authorization, string? accept, double timeout, CancellationToken cancelToken = default)
{
using var client = CreateHttpClient(authorization, accept);
using var client = CreateHttpClient(authorization, accept, timeout);
try {
using (var fs = File.Open(targetFile, FileMode.Create)) {
await DownloadToStreamInternal(client, url, fs, progress, cancelToken).ConfigureAwait(false);
@@ -35,9 +36,10 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<byte[]> DownloadBytes(string url, string? authorization, string? accept)
public virtual async Task<byte[]> DownloadBytes(string url, string? authorization, string? accept, double timeout)
{
using var client = CreateHttpClient(authorization, accept);
using var client = CreateHttpClient(authorization, accept, timeout);
try {
return await client.GetByteArrayAsync(url).ConfigureAwait(false);
} catch {
@@ -48,9 +50,10 @@ namespace Velopack.Sources
}
/// <inheritdoc />
public virtual async Task<string> DownloadString(string url, string? authorization, string? accept)
public virtual async Task<string> DownloadString(string url, string? authorization, string? accept, double timeout)
{
using var client = CreateHttpClient(authorization, accept);
using var client = CreateHttpClient(authorization, accept, timeout);
try {
return await client.GetStringAsync(url).ConfigureAwait(false);
} catch {
@@ -120,9 +123,9 @@ namespace Velopack.Sources
/// <summary>
/// Creates a new <see cref="HttpClient"/> for every request.
/// </summary>
protected virtual HttpClient CreateHttpClient(string? authorization, string? accept)
protected virtual HttpClient CreateHttpClient(string? authorization, string? accept, double timeout = 30)
{
var client = new HttpClient(CreateHttpClientHandler(), true);
var client = new HttpClient(CreateHttpClientHandler());
client.DefaultRequestHeaders.UserAgent.Add(UserAgent);
if (authorization != null)
@@ -131,6 +134,7 @@ namespace Velopack.Sources
if (accept != null)
client.DefaultRequestHeaders.Add("Accept", accept);
client.Timeout = TimeSpan.FromMinutes(timeout);
return client;
}
}

View File

@@ -25,17 +25,20 @@ namespace Velopack.Sources
/// <param name="accept">
/// Text to be sent in the 'Accept' header of the request.
/// </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, CancellationToken cancelToken = default);
Task DownloadFile(string url, string targetFile, Action<int> progress, string? authorization = null, string? accept = 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);
Task<byte[]> DownloadBytes(string url, string? authorization = null, string? accept = 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);
Task<string> DownloadString(string url, string? authorization = null, string? accept = null, double timeout = 30);
}
}

View File

@@ -14,6 +14,9 @@ namespace Velopack.Sources
/// </summary>
public class SimpleWebSource : IUpdateSource
{
/// <summary> The timeout for http requests, in minutes. </summary>
public double Timeout { get; set; }
/// <summary> The URL of the server hosting packages to update to. </summary>
public virtual Uri BaseUri { get; }
@@ -21,15 +24,16 @@ namespace Velopack.Sources
public virtual IFileDownloader Downloader { get; }
/// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(string baseUrl, IFileDownloader? downloader = null)
: this(new Uri(baseUrl), downloader)
public SimpleWebSource(string baseUrl, IFileDownloader? downloader = null, double timeout = 30)
: this(new Uri(baseUrl), downloader, timeout)
{ }
/// <inheritdoc cref="SimpleWebSource" />
public SimpleWebSource(Uri baseUri, IFileDownloader? downloader = null)
public SimpleWebSource(Uri baseUri, IFileDownloader? downloader = null, double timeout = 30)
{
BaseUri = baseUri;
Downloader = downloader ?? HttpUtil.CreateDefaultDownloader();
Timeout = timeout;
}
/// <inheritdoc />
@@ -57,7 +61,7 @@ namespace Velopack.Sources
logger.Info($"Downloading release file '{releaseFilename}' from '{uriAndQuery}'.");
var json = await Downloader.DownloadString(uriAndQuery.ToString()).ConfigureAwait(false);
var json = await Downloader.DownloadString(uriAndQuery.ToString(), timeout: Timeout).ConfigureAwait(false);
return VelopackAssetFeed.FromJson(json);
}
@@ -76,7 +80,7 @@ namespace Velopack.Sources
: HttpUtil.AppendPathToUri(sourceBaseUri, releaseEntry.FileName).ToString();
logger.Info($"Downloading '{releaseEntry.FileName}' from '{source}'.");
await Downloader.DownloadFile(source, localFile, progress, cancelToken: cancelToken).ConfigureAwait(false);
await Downloader.DownloadFile(source, localFile, progress, timeout: Timeout, cancelToken: cancelToken).ConfigureAwait(false);
}
}
}

View File

@@ -18,6 +18,8 @@ public class AzureDownloadOptions : RepositoryOptions, IObjectDownloadOptions
public string Container { get; set; }
public string SasToken { get; set; }
public double Timeout { get; set; }
}
public class AzureUploadOptions : AzureDownloadOptions, IObjectUploadOptions
@@ -39,8 +41,10 @@ public class AzureRepository : ObjectRepository<AzureDownloadOptions, AzureUploa
}
BlobServiceClient client;
// Override default timeout with user-specified value
BlobClientOptions clientOptions = new BlobClientOptions();
clientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(30);
clientOptions.Retry.NetworkTimeout = TimeSpan.FromMinutes(options.Timeout);
if (!String.IsNullOrEmpty(options.SasToken)) {
client = new BlobServiceClient(new Uri(serviceUrl), new AzureSasCredential(options.SasToken), clientOptions);

View File

@@ -16,6 +16,8 @@ public class GitHubDownloadOptions : RepositoryOptions
public string RepoUrl { get; set; }
public string Token { get; set; }
public double Timeout { get; set; }
}
public class GitHubUploadOptions : GitHubDownloadOptions
@@ -67,7 +69,7 @@ public class GitHubRepository(ILogger logger) : SourceRepository<GitHubDownloadO
Credentials = new Credentials(options.Token)
};
client.SetRequestTimeout(TimeSpan.FromHours(1));
client.SetRequestTimeout(TimeSpan.FromMinutes(options.Timeout));
var existingReleases = await client.Repository.Release.GetAll(repoOwner, repoName);
if (!options.Merge) {

View File

@@ -20,6 +20,8 @@ public class GiteaDownloadOptions : RepositoryOptions
public string Token { get; set; }
public double Timeout { get; set; }
///// <summary>
///// Example https://gitea.com
///// </summary>
@@ -40,6 +42,7 @@ public class GiteaUploadOptions : GiteaDownloadOptions
public bool Merge { get; set; }
}
public class GiteaRepository : SourceRepository<GiteaDownloadOptions, GiteaSource>, IRepositoryCanUpload<GiteaUploadOptions>
{
public GiteaRepository(ILogger logger) : base(logger)
@@ -79,6 +82,7 @@ public class GiteaRepository : SourceRepository<GiteaDownloadOptions, GiteaSourc
var uri = new Uri(options.RepoUrl);
var baseUri = uri.GetLeftPart(System.UriPartial.Authority);
config.BasePath = baseUri + "/api/v1";
config.Timeout = (int)TimeSpan.FromMinutes(options.Timeout).TotalMilliseconds;
Log.Info($"Preparing to upload {build.Files.Count} asset(s) to Gitea");

View File

@@ -6,6 +6,8 @@ namespace Velopack.Deployment;
public class HttpDownloadOptions : RepositoryOptions
{
public string Url { get; set; }
public double Timeout { get; set; }
}
public class HttpRepository : SourceRepository<HttpDownloadOptions, SimpleWebSource>
@@ -16,6 +18,6 @@ public class HttpRepository : SourceRepository<HttpDownloadOptions, SimpleWebSou
public override SimpleWebSource CreateSource(HttpDownloadOptions options)
{
return new SimpleWebSource(options.Url);
return new SimpleWebSource(options.Url, timeout: options.Timeout);
}
}

View File

@@ -21,6 +21,8 @@ public class S3DownloadOptions : RepositoryOptions, IObjectDownloadOptions
public string Bucket { get; set; }
public string Prefix { get; set; }
public double Timeout { get; set; }
}
public class S3UploadOptions : S3DownloadOptions, IObjectUploadOptions
@@ -97,6 +99,7 @@ public class S3Repository : ObjectRepository<S3DownloadOptions, S3UploadOptions,
var config = new AmazonS3Config() {
ServiceURL = options.Endpoint,
ForcePathStyle = true, // support for MINIO
Timeout = TimeSpan.FromMinutes(options.Timeout)
};
if (options.Endpoint != null) {

View File

@@ -12,6 +12,8 @@ public class AzureBaseCommand : OutputCommand
public string SasToken { get; private set; }
public double Timeout { get; private set; }
protected AzureBaseCommand(string name, string description)
: base(name, description)
{
@@ -38,6 +40,11 @@ public class AzureBaseCommand : OutputCommand
.SetArgumentHelpName("URL")
.MustBeValidHttpUri();
AddOption<double>((v) => Timeout = v, "--timeout")
.SetDescription("Network timeout in minutes.")
.SetArgumentHelpName("MINUTES")
.SetDefault(30);
this.AtLeastOneRequired(sas, key);
this.AreMutuallyExclusive(sas, key);
}

View File

@@ -6,6 +6,8 @@ public abstract class GitHubBaseCommand : OutputCommand
public string Token { get; private set; }
public double Timeout { get; private set; }
protected GitHubBaseCommand(string name, string description)
: base(name, description)
{
@@ -16,5 +18,10 @@ public abstract class GitHubBaseCommand : OutputCommand
AddOption<string>((v) => Token = v, "--token")
.SetDescription("OAuth token to use as login credentials.");
AddOption<double>((v) => Timeout = v, "--timeout")
.SetDescription("Network timeout in minutes.")
.SetArgumentHelpName("MINUTES")
.SetDefault(30);
}
}

View File

@@ -5,6 +5,8 @@ public abstract class GiteaBaseCommand : OutputCommand
public string Token { get; private set; }
public double Timeout { get; private set; }
protected GiteaBaseCommand(string name, string description)
: base(name, description)
{
@@ -15,5 +17,10 @@ public abstract class GiteaBaseCommand : OutputCommand
AddOption<string>((v) => Token = v, "--token")
.SetDescription("OAuth token to use as login credentials.");
AddOption<double>((v) => Timeout = v, "--timeout")
.SetDescription("Network timeout in minutes.")
.SetArgumentHelpName("MINUTES")
.SetDefault(30);
}
}

View File

@@ -1,9 +1,13 @@
namespace Velopack.Vpk.Commands.Deployment;
using System.Threading;
namespace Velopack.Vpk.Commands.Deployment;
public class HttpDownloadCommand : OutputCommand
{
public string Url { get; private set; }
public double Timeout { get; private set; }
public HttpDownloadCommand()
: base("http", "Download latest release from a HTTP source.")
{
@@ -11,5 +15,10 @@ public class HttpDownloadCommand : OutputCommand
.SetDescription("Url to download remote releases from.")
.MustBeValidHttpUri()
.SetRequired();
AddOption<double>((v) => Timeout = v, "--timeout")
.SetDescription("Network timeout in minutes.")
.SetArgumentHelpName("MINUTES")
.SetDefault(30);
}
}

View File

@@ -16,6 +16,8 @@ public class S3BaseCommand : OutputCommand
public string Prefix { get; private set; }
public double Timeout { get; private set; }
protected S3BaseCommand(string name, string description)
: base(name, description)
{
@@ -53,6 +55,11 @@ public class S3BaseCommand : OutputCommand
AddOption<string>((v) => Prefix = v, "--prefix")
.SetDescription("Prefix to the S3 url.")
.SetArgumentHelpName("PREFIX");
AddOption<double>((v) => Timeout = v, "--timeout")
.SetDescription("Network timeout in minutes.")
.SetArgumentHelpName("MINUTES")
.SetDefault(30);
}
private static void MustBeValidAwsRegion(OptionResult result)

View File

@@ -17,7 +17,7 @@ public class OptionMapperTests
public void MapCommand()
{
AzureUploadCommand command = new();
string cli = $"--account \"account-name\" --key \"shhhh\" --endpoint \"https://endpoint\" --container \"mycontainer\"";
string cli = $"--account \"account-name\" --key \"shhhh\" --endpoint \"https://endpoint\" --container \"mycontainer\" --timeout 45";
ParseResult parseResult = command.ParseAndApply(cli);
var options = OptionMapper.Map<AzureUploadOptions>(command);
@@ -26,5 +26,6 @@ public class OptionMapperTests
Assert.Equal("shhhh", options.Key);
Assert.Equal("https://endpoint/", options.Endpoint);
Assert.Equal("mycontainer", options.Container);
Assert.Equal(45, options.Timeout);
}
}

View File

@@ -11,7 +11,7 @@ public class FakeDownloader : Sources.IFileDownloader
public byte[] MockedResponseBytes { get; set; } = new byte[0];
public bool WriteMockLocalFile { get; set; } = false;
public Task<byte[]> DownloadBytes(string url, string auth, string acc)
public Task<byte[]> DownloadBytes(string url, string auth, string acc, double timeout = 30)
{
LastUrl = url;
LastAuthHeader = auth;
@@ -19,7 +19,7 @@ public class FakeDownloader : Sources.IFileDownloader
return Task.FromResult(MockedResponseBytes);
}
public async Task DownloadFile(string url, string targetFile, Action<int> progress, string auth, string acc, CancellationToken token)
public async Task DownloadFile(string url, string targetFile, Action<int> progress, string auth, string acc, double timeout, CancellationToken token)
{
LastLocalFile = targetFile;
var resp = await DownloadBytes(url, auth, acc);
@@ -31,7 +31,7 @@ public class FakeDownloader : Sources.IFileDownloader
File.WriteAllBytes(targetFile, resp);
}
public async Task<string> DownloadString(string url, string auth, string acc)
public async Task<string> DownloadString(string url, string auth, string acc, double timeout = 30)
{
return Encoding.UTF8.GetString(await DownloadBytes(url, auth, acc));
}

View File

@@ -55,7 +55,7 @@ internal class FakeFixtureRepository : Sources.IFileDownloader
_releases = releases;
}
public Task<byte[]> DownloadBytes(string url, string authorization = null, string accept = null)
public Task<byte[]> DownloadBytes(string url, string authorization = null, string accept = null, double timeout = 30)
{
if (url.Contains($"/{_releasesName}?")) {
MemoryStream ms = new MemoryStream();
@@ -80,7 +80,7 @@ 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, 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);
@@ -96,7 +96,7 @@ internal class FakeFixtureRepository : Sources.IFileDownloader
return Task.CompletedTask;
}
public Task<string> DownloadString(string url, string authorization = null, string accept = null)
public Task<string> DownloadString(string url, string authorization = null, string accept = null, double timeout = 30)
{
if (url.Contains($"/{_releasesName}?")) {
MemoryStream ms = new MemoryStream();