506 lines
20 KiB
C#
506 lines
20 KiB
C#
|
|
using System.Net;
|
||
|
|
using System.Net.Http.Headers;
|
||
|
|
using System.Text;
|
||
|
|
using System.Text.Json;
|
||
|
|
using Catalog.Communication.Abstractions;
|
||
|
|
using Catalog.Communication.Models;
|
||
|
|
using Microsoft.Extensions.Logging;
|
||
|
|
using Microsoft.Extensions.Options;
|
||
|
|
|
||
|
|
namespace Catalog.Communication;
|
||
|
|
|
||
|
|
public sealed class RaceUploadCommunicationClient : IRaceUploadCommunicationClient
|
||
|
|
{
|
||
|
|
private const string AdminMenuPath = "admin/menu/Menu4.abl";
|
||
|
|
private const string PublicLogonPath = "Logon.abl";
|
||
|
|
private const string UsersPath = "Users.abl";
|
||
|
|
private const string Foto2Path = "Foto2.abl";
|
||
|
|
private const string ThumbnailPath = "foto";
|
||
|
|
private const string OriginalPath = "fotoOriginali";
|
||
|
|
|
||
|
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||
|
|
{
|
||
|
|
PropertyNameCaseInsensitive = true,
|
||
|
|
};
|
||
|
|
|
||
|
|
private readonly HttpClient _httpClient;
|
||
|
|
private readonly ILogger<RaceUploadCommunicationClient> _logger;
|
||
|
|
private readonly IOptions<CatalogCommunicationOptions> _options;
|
||
|
|
|
||
|
|
public RaceUploadCommunicationClient(
|
||
|
|
HttpClient httpClient,
|
||
|
|
IOptions<CatalogCommunicationOptions> options,
|
||
|
|
ILogger<RaceUploadCommunicationClient> logger)
|
||
|
|
{
|
||
|
|
_httpClient = httpClient;
|
||
|
|
_options = options;
|
||
|
|
_logger = logger;
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> LoginAdminAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
ArgumentNullException.ThrowIfNull(request);
|
||
|
|
|
||
|
|
var formFields = new Dictionary<string, string?>
|
||
|
|
{
|
||
|
|
["login"] = request.Login,
|
||
|
|
["pwd"] = request.Password,
|
||
|
|
["cmdIU"] = request.Command,
|
||
|
|
};
|
||
|
|
|
||
|
|
return PostFormAsync(AdminMenuPath, formFields, "admin-login", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> LogoutAdminAsync(CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
var formFields = new Dictionary<string, string?>
|
||
|
|
{
|
||
|
|
["cmdIU"] = "login",
|
||
|
|
};
|
||
|
|
|
||
|
|
return PostFormAsync(AdminMenuPath, formFields, "admin-logout", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<UploadImageResponse?> UploadRaceImageAsync(RaceImageUploadRequest request, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
ArgumentNullException.ThrowIfNull(request);
|
||
|
|
|
||
|
|
return PostMultipartAndParseUploadAsync<UploadImageResponse>(
|
||
|
|
GetAdminPagePath("Gara"),
|
||
|
|
"gara-upload-image",
|
||
|
|
request.FileStream,
|
||
|
|
request.FileName,
|
||
|
|
request.FormFieldName,
|
||
|
|
request.ContentType,
|
||
|
|
static fields =>
|
||
|
|
{
|
||
|
|
fields["cmd"] = "loadImg";
|
||
|
|
},
|
||
|
|
new Dictionary<string, string?>
|
||
|
|
{
|
||
|
|
["id"] = request.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
["codImage"] = request.CodImage.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
["totImgNumber"] = request.TotImgNumber?.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
},
|
||
|
|
cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<UploadImageResponse?> RemoveRaceImageAsync(RaceImageRemoveRequest request, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
ArgumentNullException.ThrowIfNull(request);
|
||
|
|
|
||
|
|
var fields = new Dictionary<string, string?>
|
||
|
|
{
|
||
|
|
["cmd"] = "removeImg",
|
||
|
|
["id"] = request.Id.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
["codImage"] = request.CodImage.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
["totImgNumber"] = request.TotImgNumber?.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
};
|
||
|
|
|
||
|
|
return PostFormAndParseUploadAsync<UploadImageResponse>(GetAdminPagePath("Gara"), fields, "gara-remove-image", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<UploadFileResponse?> UploadRaceFileAsync(RaceFileUploadRequest request, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
ArgumentNullException.ThrowIfNull(request);
|
||
|
|
|
||
|
|
return PostMultipartAndParseUploadAsync<UploadFileResponse>(
|
||
|
|
GetAdminPagePath("Gara"),
|
||
|
|
"gara-upload-file",
|
||
|
|
request.FileStream,
|
||
|
|
request.FileName,
|
||
|
|
request.FormFieldName,
|
||
|
|
request.ContentType,
|
||
|
|
static fields =>
|
||
|
|
{
|
||
|
|
fields["cmd"] = "saveFile";
|
||
|
|
},
|
||
|
|
new Dictionary<string, string?>
|
||
|
|
{
|
||
|
|
["codFile"] = request.CodFile?.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
["id"] = request.Id?.ToString(System.Globalization.CultureInfo.InvariantCulture),
|
||
|
|
},
|
||
|
|
cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> ExecuteGaraCommandAsync(IReadOnlyDictionary<string, string?> formFields, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
return PostFormAsync(GetAdminPagePath("Gara"), formFields, "gara-command", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> ExecuteAdminPhotoCommandAsync(AdminPhotoEndpoint endpoint, IReadOnlyDictionary<string, string?> formFields, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
var path = endpoint switch
|
||
|
|
{
|
||
|
|
AdminPhotoEndpoint.Foto => GetAdminPagePath("Foto"),
|
||
|
|
AdminPhotoEndpoint.TipoGara => GetAdminPagePath("TipoGara"),
|
||
|
|
AdminPhotoEndpoint.LogFoto => GetAdminPagePath("LogFoto"),
|
||
|
|
_ => throw new ArgumentOutOfRangeException(nameof(endpoint), endpoint, "Unsupported endpoint."),
|
||
|
|
};
|
||
|
|
|
||
|
|
return PostFormAsync(path, formFields, $"photo-admin-{endpoint}", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> ExecutePublicLogonAsync(IReadOnlyDictionary<string, string?> formFields, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
return PostFormAsync(PublicLogonPath, formFields, "public-logon", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> ExecuteUsersAsync(HttpMethod method, IReadOnlyDictionary<string, string?>? formFields = null, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
return SendCommandAsync(method, UsersPath, formFields, "public-users", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<RawEndpointResponse> ExecuteFoto2Async(HttpMethod method, IReadOnlyDictionary<string, string?>? formFields = null, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
return SendCommandAsync(method, Foto2Path, formFields, "public-foto2", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<MediaFileResponse> DownloadThumbnailAsync(string filename, long? idFoto = null, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(filename);
|
||
|
|
return DownloadFileAsync(ThumbnailPath, filename, idFoto, "media-thumbnail", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
public Task<MediaFileResponse> DownloadOriginalAsync(string filename, long? idFoto = null, CancellationToken cancellationToken = default)
|
||
|
|
{
|
||
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(filename);
|
||
|
|
return DownloadFileAsync(OriginalPath, filename, idFoto, "media-original", cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<RawEndpointResponse> SendCommandAsync(HttpMethod method, string path, IReadOnlyDictionary<string, string?>? formFields, string operationName, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
ArgumentNullException.ThrowIfNull(method);
|
||
|
|
|
||
|
|
if (method == HttpMethod.Get)
|
||
|
|
{
|
||
|
|
var relativePath = AppendQuery(path, formFields);
|
||
|
|
return GetAsync(relativePath, operationName, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (method == HttpMethod.Post)
|
||
|
|
{
|
||
|
|
return PostFormAsync(path, formFields ?? new Dictionary<string, string?>(), operationName, cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new NotSupportedException($"Only GET and POST are supported. Requested method: {method}.");
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<RawEndpointResponse> GetAsync(string relativePath, string operationName, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
return ExecuteWithResilienceAsync(
|
||
|
|
() => new HttpRequestMessage(HttpMethod.Get, relativePath),
|
||
|
|
ToRawResponseAsync,
|
||
|
|
operationName,
|
||
|
|
cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<RawEndpointResponse> PostFormAsync(string relativePath, IReadOnlyDictionary<string, string?> formFields, string operationName, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
return ExecuteWithResilienceAsync(
|
||
|
|
() =>
|
||
|
|
{
|
||
|
|
var request = new HttpRequestMessage(HttpMethod.Post, relativePath)
|
||
|
|
{
|
||
|
|
Content = BuildFormContent(formFields),
|
||
|
|
};
|
||
|
|
|
||
|
|
return request;
|
||
|
|
},
|
||
|
|
ToRawResponseAsync,
|
||
|
|
operationName,
|
||
|
|
cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<TUpload?> PostFormAndParseUploadAsync<TUpload>(string relativePath, IReadOnlyDictionary<string, string?> formFields, string operationName, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
return ExecuteWithResilienceAsync(
|
||
|
|
() =>
|
||
|
|
{
|
||
|
|
var request = new HttpRequestMessage(HttpMethod.Post, relativePath)
|
||
|
|
{
|
||
|
|
Content = BuildFormContent(formFields),
|
||
|
|
};
|
||
|
|
|
||
|
|
return request;
|
||
|
|
},
|
||
|
|
async (response, token) =>
|
||
|
|
{
|
||
|
|
var raw = await ToRawResponseAsync(response, token).ConfigureAwait(false);
|
||
|
|
return ParseSingleItemArray<TUpload>(raw.Body);
|
||
|
|
},
|
||
|
|
operationName,
|
||
|
|
cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task<TUpload?> PostMultipartAndParseUploadAsync<TUpload>(
|
||
|
|
string relativePath,
|
||
|
|
string operationName,
|
||
|
|
Stream fileStream,
|
||
|
|
string fileName,
|
||
|
|
string formFieldName,
|
||
|
|
string? contentType,
|
||
|
|
Action<Dictionary<string, string?>> configureRequiredFields,
|
||
|
|
IReadOnlyDictionary<string, string?> optionalFields,
|
||
|
|
CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
ArgumentNullException.ThrowIfNull(fileStream);
|
||
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(fileName);
|
||
|
|
ArgumentException.ThrowIfNullOrWhiteSpace(formFieldName);
|
||
|
|
|
||
|
|
var payload = await ReadAllBytesAsync(fileStream, cancellationToken).ConfigureAwait(false);
|
||
|
|
|
||
|
|
return await ExecuteWithResilienceAsync(
|
||
|
|
() =>
|
||
|
|
{
|
||
|
|
var multipart = new MultipartFormDataContent();
|
||
|
|
var fields = new Dictionary<string, string?>();
|
||
|
|
configureRequiredFields(fields);
|
||
|
|
|
||
|
|
foreach (var field in fields)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(field.Value))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
multipart.Add(new StringContent(field.Value, Encoding.UTF8), field.Key);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var field in optionalFields)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(field.Value))
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
multipart.Add(new StringContent(field.Value, Encoding.UTF8), field.Key);
|
||
|
|
}
|
||
|
|
|
||
|
|
var fileContent = new ByteArrayContent(payload);
|
||
|
|
fileContent.Headers.ContentType = new MediaTypeHeaderValue(contentType ?? "application/octet-stream");
|
||
|
|
multipart.Add(fileContent, formFieldName, fileName);
|
||
|
|
|
||
|
|
return new HttpRequestMessage(HttpMethod.Post, relativePath)
|
||
|
|
{
|
||
|
|
Content = multipart,
|
||
|
|
};
|
||
|
|
},
|
||
|
|
async (response, token) =>
|
||
|
|
{
|
||
|
|
var raw = await ToRawResponseAsync(response, token).ConfigureAwait(false);
|
||
|
|
return ParseSingleItemArray<TUpload>(raw.Body);
|
||
|
|
},
|
||
|
|
operationName,
|
||
|
|
cancellationToken).ConfigureAwait(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private Task<MediaFileResponse> DownloadFileAsync(string basePath, string filename, long? idFoto, string operationName, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
var escapedFileName = Uri.EscapeDataString(filename);
|
||
|
|
var relativePath = idFoto.HasValue
|
||
|
|
? $"{basePath}/{escapedFileName}?id_foto={idFoto.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)}"
|
||
|
|
: $"{basePath}/{escapedFileName}";
|
||
|
|
|
||
|
|
return ExecuteWithResilienceAsync(
|
||
|
|
() => new HttpRequestMessage(HttpMethod.Get, relativePath),
|
||
|
|
async (response, token) =>
|
||
|
|
{
|
||
|
|
var content = await response.Content.ReadAsByteArrayAsync(token).ConfigureAwait(false);
|
||
|
|
return new MediaFileResponse
|
||
|
|
{
|
||
|
|
StatusCode = response.StatusCode,
|
||
|
|
Content = content,
|
||
|
|
ContentType = response.Content.Headers.ContentType?.MediaType,
|
||
|
|
FileName = response.Content.Headers.ContentDisposition?.FileNameStar ?? response.Content.Headers.ContentDisposition?.FileName,
|
||
|
|
Headers = BuildHeaders(response),
|
||
|
|
};
|
||
|
|
},
|
||
|
|
operationName,
|
||
|
|
cancellationToken);
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task<TResponse> ExecuteWithResilienceAsync<TResponse>(
|
||
|
|
Func<HttpRequestMessage> requestFactory,
|
||
|
|
Func<HttpResponseMessage, CancellationToken, Task<TResponse>> responseFactory,
|
||
|
|
string operationName,
|
||
|
|
CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
var options = _options.Value;
|
||
|
|
Exception? lastException = null;
|
||
|
|
|
||
|
|
for (var attempt = 0; attempt <= options.RetryCount; attempt++)
|
||
|
|
{
|
||
|
|
using var request = requestFactory();
|
||
|
|
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||
|
|
timeoutCts.CancelAfter(options.RequestTimeout);
|
||
|
|
|
||
|
|
HttpResponseMessage? response = null;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
response = await _httpClient
|
||
|
|
.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, timeoutCts.Token)
|
||
|
|
.ConfigureAwait(false);
|
||
|
|
|
||
|
|
if (IsRetryableStatusCode(response.StatusCode) && attempt < options.RetryCount)
|
||
|
|
{
|
||
|
|
_logger.LogWarning(
|
||
|
|
"Operation {OperationName} received retryable status code {StatusCode} at attempt {Attempt}/{MaxAttempts}.",
|
||
|
|
operationName,
|
||
|
|
(int)response.StatusCode,
|
||
|
|
attempt + 1,
|
||
|
|
options.RetryCount + 1);
|
||
|
|
|
||
|
|
await DelayBeforeRetryAsync(options, attempt, cancellationToken).ConfigureAwait(false);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
return await responseFactory(response, cancellationToken).ConfigureAwait(false);
|
||
|
|
}
|
||
|
|
catch (OperationCanceledException ex) when (!cancellationToken.IsCancellationRequested)
|
||
|
|
{
|
||
|
|
lastException = ex;
|
||
|
|
|
||
|
|
if (attempt < options.RetryCount)
|
||
|
|
{
|
||
|
|
_logger.LogWarning(ex, "Operation {OperationName} timed out at attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, options.RetryCount + 1);
|
||
|
|
await DelayBeforeRetryAsync(options, attempt, cancellationToken).ConfigureAwait(false);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
_logger.LogError(ex, "Operation {OperationName} timed out after {MaxAttempts} attempts.", operationName, options.RetryCount + 1);
|
||
|
|
throw;
|
||
|
|
}
|
||
|
|
catch (HttpRequestException ex)
|
||
|
|
{
|
||
|
|
lastException = ex;
|
||
|
|
|
||
|
|
if (attempt < options.RetryCount)
|
||
|
|
{
|
||
|
|
_logger.LogWarning(ex, "Operation {OperationName} failed with transient HTTP error at attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, options.RetryCount + 1);
|
||
|
|
await DelayBeforeRetryAsync(options, attempt, cancellationToken).ConfigureAwait(false);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
_logger.LogError(ex, "Operation {OperationName} failed with HTTP error after {MaxAttempts} attempts.", operationName, options.RetryCount + 1);
|
||
|
|
throw;
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Operation {OperationName} failed with unexpected error at attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, options.RetryCount + 1);
|
||
|
|
throw;
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
response?.Dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
throw new HttpRequestException($"Operation '{operationName}' failed after {options.RetryCount + 1} attempts.", lastException);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static bool IsRetryableStatusCode(HttpStatusCode statusCode)
|
||
|
|
{
|
||
|
|
return statusCode is HttpStatusCode.RequestTimeout
|
||
|
|
or HttpStatusCode.TooManyRequests
|
||
|
|
or HttpStatusCode.BadGateway
|
||
|
|
or HttpStatusCode.ServiceUnavailable
|
||
|
|
or HttpStatusCode.GatewayTimeout
|
||
|
|
or HttpStatusCode.InternalServerError;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task DelayBeforeRetryAsync(CatalogCommunicationOptions options, int attempt, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
var delay = TimeSpan.FromMilliseconds(options.RetryBaseDelay.TotalMilliseconds * Math.Pow(2, attempt));
|
||
|
|
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static FormUrlEncodedContent BuildFormContent(IReadOnlyDictionary<string, string?> formFields)
|
||
|
|
{
|
||
|
|
var pairs = formFields
|
||
|
|
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
|
||
|
|
.Select(kvp => new KeyValuePair<string, string>(kvp.Key, kvp.Value!));
|
||
|
|
|
||
|
|
return new FormUrlEncodedContent(pairs);
|
||
|
|
}
|
||
|
|
|
||
|
|
private static string AppendQuery(string path, IReadOnlyDictionary<string, string?>? query)
|
||
|
|
{
|
||
|
|
if (query is null || query.Count == 0)
|
||
|
|
{
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
var encodedPairs = query
|
||
|
|
.Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value))
|
||
|
|
.Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}")
|
||
|
|
.ToArray();
|
||
|
|
|
||
|
|
if (encodedPairs.Length == 0)
|
||
|
|
{
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
|
||
|
|
var separator = path.Contains('?', StringComparison.Ordinal) ? "&" : "?";
|
||
|
|
return string.Concat(path, separator, string.Join("&", encodedPairs));
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task<RawEndpointResponse> ToRawResponseAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||
|
|
|
||
|
|
return new RawEndpointResponse
|
||
|
|
{
|
||
|
|
StatusCode = response.StatusCode,
|
||
|
|
Body = body,
|
||
|
|
Headers = BuildHeaders(response),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
private static IReadOnlyDictionary<string, IReadOnlyList<string>> BuildHeaders(HttpResponseMessage response)
|
||
|
|
{
|
||
|
|
var headers = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||
|
|
|
||
|
|
foreach (var header in response.Headers)
|
||
|
|
{
|
||
|
|
headers[header.Key] = header.Value.ToArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (var header in response.Content.Headers)
|
||
|
|
{
|
||
|
|
headers[header.Key] = header.Value.ToArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
return headers;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static TUpload? ParseSingleItemArray<TUpload>(string json)
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(json))
|
||
|
|
{
|
||
|
|
return default;
|
||
|
|
}
|
||
|
|
|
||
|
|
var items = JsonSerializer.Deserialize<List<TUpload>>(json, JsonOptions);
|
||
|
|
return items is { Count: > 0 } ? items[0] : default;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static async Task<byte[]> ReadAllBytesAsync(Stream stream, CancellationToken cancellationToken)
|
||
|
|
{
|
||
|
|
if (stream.CanSeek)
|
||
|
|
{
|
||
|
|
stream.Position = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
using var memoryStream = new MemoryStream();
|
||
|
|
await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||
|
|
return memoryStream.ToArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
private string GetAdminPagePath(string pageName)
|
||
|
|
{
|
||
|
|
var basePath = _options.Value.AdminPageBasePath.Trim('/');
|
||
|
|
return $"{basePath}/{pageName}.abl";
|
||
|
|
}
|
||
|
|
}
|