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
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
|
|
}
|
|
}
|