You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

367 lines
16 KiB

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using Minio.Credentials;
using Minio.DataModel;
using Minio.DataModel.Args;
using Minio.DataModel.Result;
using Minio.Exceptions;
using Minio.Handlers;
using Minio.Helper;
namespace Minio;
public static class RequestExtensions
{
[SuppressMessage("Design", "CA1054:URI-like parameters should not be strings",
Justification = "This is done in the interface. String is provided here for convenience")]
public static Task<HttpResponseMessage> WrapperGetAsync(this IMinioClient minioClient, string url)
{
return minioClient is null
? throw new ArgumentNullException(nameof(minioClient))
: minioClient.WrapperGetAsync(new Uri(url));
}
/// <summary>
/// Runs httpClient's PutObjectAsync method
/// </summary>
[SuppressMessage("Design", "CA1054:URI-like parameters should not be strings",
Justification = "This is done in the interface. String is provided here for convenience")]
public static Task WrapperPutAsync(this IMinioClient minioClient, string url, StreamContent strm)
{
return minioClient is null
? throw new ArgumentNullException(nameof(minioClient))
: minioClient.WrapperPutAsync(new Uri(url), strm);
}
/// <summary>
/// Actual doer that executes the request on the server
/// </summary>
/// <param name="minioClient"></param>
/// <param name="requestMessageBuilder">The build of HttpRequestMessageBuilder </param>
/// <param name="ignoreExceptionType">any type of Exception; if an exception type is going to be ignored</param>
/// <param name="isSts">boolean; if true role credentials, otherwise IAM user</param>
/// <param name="cancellationToken">Optional cancellation token to cancel the operation</param>
/// <returns>ResponseResult</returns>
internal static async Task<ResponseResult> ExecuteTaskAsync(this IMinioClient minioClient,
HttpRequestMessageBuilder requestMessageBuilder,
Type ignoreExceptionType = null,
bool isSts = false,
CancellationToken cancellationToken = default)
{
var startTime = DateTime.Now;
var responseResult = new ResponseResult(requestMessageBuilder.Request, response: null);
using var internalTokenSource =
new CancellationTokenSource(new TimeSpan(0, 0, 0, 0, minioClient.Config.RequestTimeout));
using var timeoutTokenSource =
CancellationTokenSource.CreateLinkedTokenSource(internalTokenSource.Token, cancellationToken);
if (minioClient.Config.RequestTimeout > 0) cancellationToken = timeoutTokenSource.Token;
responseResult = await minioClient.ExecuteWithRetry(
async Task<ResponseResult> () => await minioClient.ExecuteTaskCoreAsync(
requestMessageBuilder,
isSts, cancellationToken).ConfigureAwait(false)).ConfigureAwait(false);
if ((responseResult is not null &&
!Equals(responseResult.Exception?.GetType(), ignoreExceptionType)) ||
responseResult.StatusCode != HttpStatusCode.OK)
{
var handler = new DefaultErrorHandler();
handler.Handle(responseResult);
}
return responseResult;
}
private static async Task<ResponseResult> ExecuteTaskCoreAsync(this IMinioClient minioClient,
// IEnumerable<IApiResponseErrorHandler> errorHandlers,
HttpRequestMessageBuilder requestMessageBuilder,
bool isSts = false,
CancellationToken cancellationToken = default)
{
var startTime = Stopwatch.GetTimestamp();
var v4Authenticator = new V4Authenticator(minioClient.Config.Secure,
minioClient.Config.AccessKey, minioClient.Config.SecretKey, minioClient.Config.Region,
minioClient.Config.SessionToken);
requestMessageBuilder.AddOrUpdateHeaderParameter("Authorization",
v4Authenticator.Authenticate(requestMessageBuilder, isSts));
var request = requestMessageBuilder.Request;
var responseResult = new ResponseResult(request, new HttpResponseMessage());
try
{
var response = await minioClient.Config.HttpClient.SendAsync(request,
HttpCompletionOption.ResponseHeadersRead,
cancellationToken).ConfigureAwait(false);
responseResult = new ResponseResult(request, response);
if (requestMessageBuilder.ResponseWriter is not null)
await requestMessageBuilder.ResponseWriter(responseResult.ContentStream, cancellationToken)
.ConfigureAwait(false);
}
catch (Exception ex)
{
responseResult.Exception = ex;
}
return responseResult;
}
private static async Task<ResponseResult> ExecuteWithRetry(this IMinioClient minioClient,
Func<Task<ResponseResult>> executeRequestCallback)
{
return minioClient.Config.RetryPolicyHandler is null
? await executeRequestCallback().ConfigureAwait(false)
: await minioClient.Config.RetryPolicyHandler.Handle(executeRequestCallback).ConfigureAwait(false);
}
/// <summary>
/// Constructs a HttpRequestMessageBuilder using bucket/object names from Args.
/// Calls overloaded CreateRequest method.
/// </summary>
/// <param name="minioClient"></param>
/// <param name="args">The direct descendant of BucketArgs class, args with populated values from Input</param>
/// <returns>A HttpRequestMessageBuilder</returns>
internal static async Task<HttpRequestMessageBuilder> CreateRequest<T>(this IMinioClient minioClient,
BucketArgs<T> args) where T : BucketArgs<T>
{
ArgsCheck(args);
var requestMessageBuilder =
await minioClient.CreateRequest(args.RequestMethod, args.BucketName, headerMap: args.Headers,
isBucketCreationRequest: args.IsBucketCreationRequest).ConfigureAwait(false);
return args.BuildRequest(requestMessageBuilder);
}
/// <summary>
/// Constructs a HttpRequestMessage using bucket/object names from Args.
/// Calls overloaded CreateRequest method.
/// </summary>
/// <param name="minioClient"></param>
/// <param name="args">The direct descendant of ObjectArgs class, args with populated values from Input</param>
/// <returns>A HttpRequestMessage</returns>
internal static async Task<HttpRequestMessageBuilder> CreateRequest<T>(this IMinioClient minioClient,
ObjectArgs<T> args) where T : ObjectArgs<T>
{
ArgsCheck(args);
var contentType = "application/octet-stream";
_ = args.Headers?.TryGetValue("Content-Type", out contentType);
var requestMessageBuilder =
await minioClient.CreateRequest(args.RequestMethod,
args.BucketName,
args.ObjectName,
args.Headers,
contentType,
args.RequestBody).ConfigureAwait(false);
return args.BuildRequest(requestMessageBuilder);
}
/// <summary>
/// Constructs an HttpRequestMessage builder. For AWS, this function
/// has the side-effect of overriding the baseUrl in the HttpClient
/// with region specific host path or virtual style path.
/// </summary>
/// <param name="minioClient"></param>
/// <param name="method">HTTP method</param>
/// <param name="bucketName">Bucket Name</param>
/// <param name="objectName">Object Name</param>
/// <param name="headerMap">headerMap</param>
/// <param name="contentType">Content Type</param>
/// <param name="body">request body</param>
/// <param name="resourcePath">query string</param>
/// <param name="isBucketCreationRequest">boolean to define bucket creation</param>
/// <returns>A HttpRequestMessage builder</returns>
/// <exception cref="BucketNotFoundException">When bucketName is invalid</exception>
internal static async Task<HttpRequestMessageBuilder> CreateRequest(this IMinioClient minioClient,
HttpMethod method,
string bucketName = null,
string objectName = null,
IDictionary<string, string> headerMap = null,
string contentType = "application/octet-stream",
ReadOnlyMemory<byte> body = default,
string resourcePath = null,
bool isBucketCreationRequest = false)
{
var region = string.Empty;
if (bucketName is not null)
{
Utils.ValidateBucketName(bucketName);
// Fetch correct region for bucket if this is not a bucket creation
if (!isBucketCreationRequest)
region = await minioClient.GetRegion(bucketName).ConfigureAwait(false);
}
if (objectName is not null) Utils.ValidateObjectName(objectName);
if (minioClient.Config.Provider is not null)
{
var isAWSEnvProvider = minioClient.Config.Provider is AWSEnvironmentProvider ||
(minioClient.Config.Provider is ChainedProvider ch &&
ch.CurrentProvider is AWSEnvironmentProvider);
var isIAMAWSProvider = minioClient.Config.Provider is IAMAWSProvider ||
(minioClient.Config.Provider is ChainedProvider chained &&
chained.CurrentProvider is AWSEnvironmentProvider);
AccessCredentials creds;
if (isAWSEnvProvider)
{
var aWSEnvProvider = (AWSEnvironmentProvider)minioClient.Config.Provider;
creds = await aWSEnvProvider.GetCredentialsAsync().ConfigureAwait(false);
}
else if (isIAMAWSProvider)
{
var iamAWSProvider = (IAMAWSProvider)minioClient.Config.Provider;
creds = iamAWSProvider.Credentials;
}
else
{
creds = await minioClient.Config.Provider.GetCredentialsAsync().ConfigureAwait(false);
}
if (creds is not null)
{
minioClient.Config.AccessKey = creds.AccessKey;
minioClient.Config.SecretKey = creds.SecretKey;
}
}
// This section reconstructs the url with scheme followed by location specific endpoint (s3.region.amazonaws.com)
// or Virtual Host styled endpoint (bucketname.s3.region.amazonaws.com) for Amazon requests.
var resource = string.Empty;
var usePathStyle = false;
if (!string.IsNullOrEmpty(bucketName) && S3utils.IsAmazonEndPoint(minioClient.Config.BaseUrl))
{
if (method == HttpMethod.Put && objectName is null && resourcePath is null)
// use path style for make bucket to workaround "AuthorizationHeaderMalformed" error from s3.amazonaws.com
usePathStyle = true;
else if (resourcePath?.Contains("location", StringComparison.OrdinalIgnoreCase) == true)
// use path style for location query
usePathStyle = true;
else if (bucketName.Contains('.', StringComparison.Ordinal) && minioClient.Config.Secure)
// use path style where '.' in bucketName causes SSL certificate validation error
usePathStyle = true;
if (usePathStyle) resource += Utils.UrlEncode(bucketName) + "/";
}
// Set Target URL
var requestUrl = RequestUtil.MakeTargetURL(minioClient.Config.BaseUrl, minioClient.Config.Secure, bucketName,
region, usePathStyle);
if (objectName is not null) resource += Utils.EncodePath(objectName);
// Append query string passed in
if (resourcePath is not null) resource += resourcePath;
HttpRequestMessageBuilder messageBuilder;
if (!string.IsNullOrEmpty(resource))
messageBuilder = new HttpRequestMessageBuilder(method, requestUrl, resource);
else
messageBuilder = new HttpRequestMessageBuilder(method, requestUrl);
if (!body.IsEmpty)
{
messageBuilder.SetBody(body);
messageBuilder.AddOrUpdateHeaderParameter("Content-Type", contentType);
}
if (headerMap?.Count > 0)
{
if (headerMap.TryGetValue(messageBuilder.ContentTypeKey, out var value) && !string.IsNullOrEmpty(value))
headerMap[messageBuilder.ContentTypeKey] = contentType;
foreach (var entry in headerMap) messageBuilder.AddOrUpdateHeaderParameter(entry.Key, entry.Value);
}
return messageBuilder;
}
/// <summary>
/// Null Check for Args object.
/// Expected to be called from CreateRequest
/// </summary>
/// <param name="args">The child object of Args class</param>
private static void ArgsCheck(RequestArgs args)
{
if (args is null)
throw new ArgumentNullException(nameof(args),
"Args object cannot be null. It needs to be assigned to an instantiated child object of Args.");
}
/// <summary>
/// Resolve region of the bucket.
/// </summary>
/// <param name="minioClient"></param>
/// <param name="bucketName"></param>
/// <returns></returns>
internal static async Task<string> GetRegion(this IMinioClient minioClient, string bucketName)
{
var rgn = "";
// Use user specified region in client constructor if present
if (!string.IsNullOrEmpty(minioClient.Config.Region)) return minioClient.Config.Region;
// pick region from endpoint if present
if (!string.IsNullOrEmpty(minioClient.Config.Endpoint))
rgn = RegionHelper.GetRegionFromEndpoint(minioClient.Config.Endpoint);
// Pick region from location HEAD request
if (rgn?.Length == 0)
rgn = BucketRegionCache.Instance.Exists(bucketName)
? await BucketRegionCache.Update(minioClient, bucketName).ConfigureAwait(false)
: BucketRegionCache.Instance.Region(bucketName);
// Defaults to us-east-1 if region could not be found
return rgn?.Length == 0 ? "us-east-1" : rgn;
}
/// <summary>
/// Delegate errors to handlers
/// </summary>
/// <param name="minioClient"></param>
/// <param name="response"></param>
/// <param name="handlers"></param>
/// <param name="startTime"></param>
/// <param name="ignoreExceptionType"></param>
private static void HandleIfErrorResponse(this IMinioClient minioClient, ResponseResult response,
IEnumerable<IApiResponseErrorHandler> handlers,
long startTime,
Type ignoreExceptionType = null)
{
// Logs Response if HTTP tracing is enabled
if (minioClient.Config.TraceHttp)
{
var elapsed = GetElapsedTime(startTime);
minioClient.LogRequest(response.Request, response, elapsed.TotalMilliseconds);
}
if (response.Exception is not null)
{
if (response.Exception?.GetType() == ignoreExceptionType)
{
response.Exception = null;
}
else
{
if (handlers.Any())
// Run through handlers passed to take up error handling
foreach (var handler in handlers)
handler.Handle(response);
else
minioClient.DefaultErrorHandler.Handle(response);
}
}
}
private static TimeSpan GetElapsedTime(long startTimestamp)
{
#if NET8_0_OR_GREATER
return Stopwatch.GetElapsedTime(startTimestamp);
#else
var endTimestamp = Stopwatch.GetTimestamp();
var elapsedTicks = endTimestamp - startTimestamp;
var seconds = (double)elapsedTicks / Stopwatch.Frequency;
return TimeSpan.FromSeconds(seconds);
#endif
}
}