mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
fix.plonds-s3-upload-timeout
This commit is contained in:
@@ -27,11 +27,16 @@ public sealed class PlondsS3Client : IDisposable
|
|||||||
AccessKey = Require(options.AccessKey, nameof(options.AccessKey)),
|
AccessKey = Require(options.AccessKey, nameof(options.AccessKey)),
|
||||||
SecretKey = Require(options.SecretKey, nameof(options.SecretKey)),
|
SecretKey = Require(options.SecretKey, nameof(options.SecretKey)),
|
||||||
PublicBaseUrl = Require(options.PublicBaseUrl, nameof(options.PublicBaseUrl)).TrimEnd('/'),
|
PublicBaseUrl = Require(options.PublicBaseUrl, nameof(options.PublicBaseUrl)).TrimEnd('/'),
|
||||||
PublicBaseKeyPrefix = NormalizeOptionalKeyPrefix(options.PublicBaseKeyPrefix)
|
PublicBaseKeyPrefix = NormalizeOptionalKeyPrefix(options.PublicBaseKeyPrefix),
|
||||||
|
RequestTimeout = options.RequestTimeout <= TimeSpan.Zero ? TimeSpan.FromMinutes(30) : options.RequestTimeout,
|
||||||
|
MaxUploadAttempts = Math.Max(1, options.MaxUploadAttempts)
|
||||||
};
|
};
|
||||||
|
|
||||||
this.httpClient = httpClient ?? new HttpClient();
|
|
||||||
ownsHttpClient = httpClient is null;
|
ownsHttpClient = httpClient is null;
|
||||||
|
this.httpClient = httpClient ?? new HttpClient
|
||||||
|
{
|
||||||
|
Timeout = this.options.RequestTimeout
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UploadFileAsync(PlondsS3ObjectUpload upload, CancellationToken cancellationToken = default)
|
public async Task UploadFileAsync(PlondsS3ObjectUpload upload, CancellationToken cancellationToken = default)
|
||||||
@@ -47,20 +52,47 @@ public sealed class PlondsS3Client : IDisposable
|
|||||||
var key = NormalizeKey(upload.Key);
|
var key = NormalizeKey(upload.Key);
|
||||||
var payloadHash = PayloadUtilities.ComputeSha256(sourcePath);
|
var payloadHash = PayloadUtilities.ComputeSha256(sourcePath);
|
||||||
var contentLength = new FileInfo(sourcePath).Length;
|
var contentLength = new FileInfo(sourcePath).Length;
|
||||||
|
|
||||||
|
for (var attempt = 1; attempt <= options.MaxUploadAttempts; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await UploadFileOnceAsync(sourcePath, key, upload.ContentType, payloadHash, contentLength, attempt, cancellationToken).ConfigureAwait(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (attempt < options.MaxUploadAttempts && IsRetriable(ex))
|
||||||
|
{
|
||||||
|
var delay = TimeSpan.FromSeconds(Math.Min(30, Math.Pow(2, attempt)));
|
||||||
|
Console.Error.WriteLine($"S3 upload retry {attempt + 1}/{options.MaxUploadAttempts} for {key} after {delay.TotalSeconds:0}s: {ex.Message}");
|
||||||
|
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UploadFileOnceAsync(
|
||||||
|
string sourcePath,
|
||||||
|
string key,
|
||||||
|
string? contentType,
|
||||||
|
string payloadHash,
|
||||||
|
long contentLength,
|
||||||
|
int attempt,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var requestUri = BuildObjectUri(key);
|
var requestUri = BuildObjectUri(key);
|
||||||
|
Console.WriteLine($"Uploading S3 object {key} ({FormatBytes(contentLength)}), attempt {attempt}/{options.MaxUploadAttempts}.");
|
||||||
|
|
||||||
using var content = new StreamContent(File.OpenRead(sourcePath));
|
await using var fileStream = File.OpenRead(sourcePath);
|
||||||
content.Headers.ContentType = new MediaTypeHeaderValue(string.IsNullOrWhiteSpace(upload.ContentType)
|
using var content = new StreamContent(fileStream);
|
||||||
|
content.Headers.ContentType = new MediaTypeHeaderValue(string.IsNullOrWhiteSpace(contentType)
|
||||||
? "application/octet-stream"
|
? "application/octet-stream"
|
||||||
: upload.ContentType);
|
: contentType);
|
||||||
content.Headers.ContentLength = contentLength;
|
content.Headers.ContentLength = contentLength;
|
||||||
|
|
||||||
using var request = new HttpRequestMessage(HttpMethod.Put, requestUri)
|
using var request = new HttpRequestMessage(HttpMethod.Put, requestUri)
|
||||||
{
|
{
|
||||||
Content = content
|
Content = content
|
||||||
};
|
};
|
||||||
|
|
||||||
SignRequest(request, key, payloadHash, now);
|
SignRequest(request, key, payloadHash, now);
|
||||||
|
|
||||||
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
using var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -69,6 +101,8 @@ public sealed class PlondsS3Client : IDisposable
|
|||||||
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||||
throw new InvalidOperationException($"S3 upload failed for {key}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}. {Truncate(body, 512)}");
|
throw new InvalidOperationException($"S3 upload failed for {key}: HTTP {(int)response.StatusCode} {response.ReasonPhrase}. {Truncate(body, 512)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Uploaded S3 object {key}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task EnsureObjectExistsAsync(string key, CancellationToken cancellationToken = default)
|
public async Task EnsureObjectExistsAsync(string key, CancellationToken cancellationToken = default)
|
||||||
@@ -257,4 +291,28 @@ public sealed class PlondsS3Client : IDisposable
|
|||||||
|
|
||||||
return value[..maxLength];
|
return value[..maxLength];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsRetriable(Exception exception)
|
||||||
|
{
|
||||||
|
if (exception is TaskCanceledException or TimeoutException or HttpRequestException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return exception.InnerException is not null && IsRetriable(exception.InnerException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatBytes(long bytes)
|
||||||
|
{
|
||||||
|
string[] units = ["B", "KB", "MB", "GB"];
|
||||||
|
double value = bytes;
|
||||||
|
var unit = 0;
|
||||||
|
while (value >= 1024 && unit < units.Length - 1)
|
||||||
|
{
|
||||||
|
value /= 1024;
|
||||||
|
unit++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{value:0.##} {units[unit]}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,4 +7,9 @@ public sealed record PlondsS3ClientOptions(
|
|||||||
string AccessKey,
|
string AccessKey,
|
||||||
string SecretKey,
|
string SecretKey,
|
||||||
string PublicBaseUrl,
|
string PublicBaseUrl,
|
||||||
string PublicBaseKeyPrefix = "");
|
string PublicBaseKeyPrefix = "")
|
||||||
|
{
|
||||||
|
public TimeSpan RequestTimeout { get; init; } = TimeSpan.FromMinutes(30);
|
||||||
|
|
||||||
|
public int MaxUploadAttempts { get; init; } = 3;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user