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.
324 lines
14 KiB
324 lines
14 KiB
/*
|
|
* MinIO .NET Library for Amazon S3 Compatible Cloud Storage,
|
|
* (C) 2017-2021 MinIO, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
using System.Net;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging;
|
|
using Minio.DataModel.Result;
|
|
using Minio.Exceptions;
|
|
using Minio.Handlers;
|
|
using Minio.Helper;
|
|
|
|
namespace Minio;
|
|
|
|
public partial class MinioClient : IMinioClient
|
|
{
|
|
private static readonly char[] separator = { '/' };
|
|
|
|
private bool disposedValue;
|
|
|
|
/// <summary>
|
|
/// Creates and returns an MinIO Client
|
|
/// </summary>
|
|
/// <returns>Client with no arguments to be used with other builder methods</returns>
|
|
public MinioClient()
|
|
{
|
|
}
|
|
|
|
public MinioConfig Config { get; } = new();
|
|
|
|
public IEnumerable<IApiResponseErrorHandler> ResponseErrorHandlers { get; internal set; } =
|
|
Enumerable.Empty<IApiResponseErrorHandler>();
|
|
|
|
/// <summary>
|
|
/// Default error handling delegate
|
|
/// </summary>
|
|
public IApiResponseErrorHandler DefaultErrorHandler { get; internal set; } = new DefaultErrorHandler();
|
|
|
|
public IRequestLogger RequestLogger { get; internal set; }
|
|
|
|
/// <summary>
|
|
/// Runs httpClient's GetAsync method
|
|
/// </summary>
|
|
public Task<HttpResponseMessage> WrapperGetAsync(Uri uri)
|
|
{
|
|
return Config.HttpClient.GetAsync(uri);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs httpClient's PutObjectAsync method
|
|
/// </summary>
|
|
public Task WrapperPutAsync(Uri uri, StreamContent strm)
|
|
{
|
|
return Task.Run(async () => await Config.HttpClient.PutAsync(uri, strm).ConfigureAwait(false));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets HTTP tracing On.Writes output to Console
|
|
/// </summary>
|
|
public void SetTraceOn(IRequestLogger requestLogger = null)
|
|
{
|
|
var logger = Config?.ServiceProvider?.GetRequiredService<ILogger<DefaultRequestLogger>>();
|
|
RequestLogger = requestLogger ?? new DefaultRequestLogger(logger);
|
|
Config.TraceHttp = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets HTTP tracing Off.
|
|
/// </summary>
|
|
public void SetTraceOff()
|
|
{
|
|
Config.TraceHttp = false;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
Dispose(true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse response errors if any and return relevant error messages
|
|
/// </summary>
|
|
/// <param name="response"></param>
|
|
internal static void ParseError(ResponseResult response)
|
|
{
|
|
if (response is null)
|
|
throw new ConnectionException(
|
|
"Response is nil. Please report this issue https://github.com/minio/minio-dotnet/issues", response);
|
|
|
|
if (HttpStatusCode.Redirect == response.StatusCode ||
|
|
HttpStatusCode.TemporaryRedirect == response.StatusCode ||
|
|
HttpStatusCode.MovedPermanently == response.StatusCode)
|
|
throw new RedirectionException(
|
|
"Redirection detected. Please report this issue https://github.com/minio/minio-dotnet/issues");
|
|
|
|
if (string.IsNullOrWhiteSpace(response.Content))
|
|
{
|
|
ParseErrorNoContent(response);
|
|
return;
|
|
}
|
|
|
|
ParseErrorFromContent(response);
|
|
}
|
|
|
|
private static void ParseErrorNoContent(ResponseResult response)
|
|
{
|
|
if (response is null)
|
|
throw new ArgumentNullException(nameof(response));
|
|
var statusCodeStrs = new[]
|
|
{
|
|
nameof(HttpStatusCode.Forbidden), nameof(HttpStatusCode.BadRequest), nameof(HttpStatusCode.NotFound),
|
|
nameof(HttpStatusCode.MethodNotAllowed), nameof(HttpStatusCode.NotImplemented)
|
|
};
|
|
|
|
if (response.Exception != null && !string.IsNullOrEmpty(response.ErrorMessage))
|
|
{
|
|
foreach (var exception in statusCodeStrs)
|
|
if ((response.ErrorMessage?.Contains(exception, StringComparison.InvariantCulture) ?? false) ||
|
|
(response.ErrorMessage?.Contains(response.StatusCode.ToString(),
|
|
StringComparison.InvariantCulture) ?? false))
|
|
{
|
|
ParseWellKnownErrorNoContent(response);
|
|
break;
|
|
}
|
|
}
|
|
else if (statusCodeStrs.Contains(response.StatusCode.ToString(), StringComparer.Ordinal))
|
|
{
|
|
ParseWellKnownErrorNoContent(response);
|
|
}
|
|
#pragma warning disable MA0099 // Use Explicit enum value instead of 0
|
|
if (response.StatusCode == 0)
|
|
throw new ConnectionException("Connection error:" + response.ErrorMessage, response);
|
|
if (response.Exception.GetType() == typeof(TaskCanceledException))
|
|
throw response.Exception;
|
|
#pragma warning restore MA0099 // Use Explicit enum value instead of 0
|
|
throw new InternalClientException(
|
|
"Unsuccessful response from server without XML:" + response.ErrorMessage, response);
|
|
}
|
|
|
|
private static void ParseWellKnownErrorNoContent(ResponseResult response)
|
|
{
|
|
MinioException error = null;
|
|
var errorResponse = new ErrorResponse();
|
|
|
|
foreach (var parameter in response.Headers)
|
|
{
|
|
if (parameter.Key.Equals("x-amz-id-2", StringComparison.OrdinalIgnoreCase))
|
|
errorResponse.HostId = parameter.Value;
|
|
|
|
if (parameter.Key.Equals("x-amz-request-id", StringComparison.OrdinalIgnoreCase))
|
|
errorResponse.RequestId = parameter.Value;
|
|
|
|
if (parameter.Key.Equals("x-amz-bucket-region", StringComparison.OrdinalIgnoreCase))
|
|
errorResponse.BucketRegion = parameter.Value;
|
|
}
|
|
|
|
var pathAndQuery = response.Request.RequestUri.PathAndQuery;
|
|
var host = response.Request.RequestUri.Host;
|
|
errorResponse.Resource = pathAndQuery;
|
|
|
|
// zero, one or two segments
|
|
var resourceSplits = pathAndQuery.Split(separator, 2, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.Ordinal) ||
|
|
response.Exception.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.Ordinal))
|
|
{
|
|
var pathLength = resourceSplits.Length;
|
|
var isAWS = host.EndsWith("s3.amazonaws.com", StringComparison.OrdinalIgnoreCase);
|
|
var isVirtual = isAWS && !host.StartsWith("s3.amazonaws.com", StringComparison.OrdinalIgnoreCase);
|
|
|
|
if (pathLength > 1)
|
|
{
|
|
var objectName = resourceSplits[1];
|
|
errorResponse.Code = "NoSuchKey";
|
|
error = new ObjectNotFoundException(objectName);
|
|
}
|
|
else if (pathLength == 1)
|
|
{
|
|
var resource = resourceSplits[0];
|
|
|
|
if (isAWS && isVirtual)
|
|
{
|
|
errorResponse.Code = "NoSuchKey";
|
|
error = new ObjectNotFoundException(resource);
|
|
}
|
|
else
|
|
{
|
|
errorResponse.Code = "NoSuchBucket";
|
|
BucketRegionCache.Instance.Remove(resource);
|
|
error = new BucketNotFoundException(resource);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
error = new InternalClientException("404 without body resulted in path with less than two components",
|
|
response);
|
|
}
|
|
}
|
|
else if (HttpStatusCode.BadRequest == response.StatusCode)
|
|
{
|
|
var pathLength = resourceSplits.Length;
|
|
|
|
if (pathLength > 1)
|
|
{
|
|
var objectName = resourceSplits[1];
|
|
errorResponse.Code = "InvalidObjectName";
|
|
error = new InvalidObjectNameException(objectName, "Invalid object name.");
|
|
}
|
|
else
|
|
{
|
|
error = new InternalClientException("400 without body resulted in path with less than two components",
|
|
response);
|
|
}
|
|
}
|
|
else if (HttpStatusCode.Forbidden == response.StatusCode)
|
|
{
|
|
errorResponse.Code = "Forbidden";
|
|
error = new AccessDeniedException("Access denied on the resource: " + pathAndQuery);
|
|
}
|
|
|
|
response.Exception = error;
|
|
throw error;
|
|
}
|
|
|
|
private static void ParseErrorFromContent(ResponseResult response)
|
|
{
|
|
if (response is null)
|
|
throw new ArgumentNullException(nameof(response));
|
|
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.OrdinalIgnoreCase)
|
|
&& response.Request.RequestUri.PathAndQuery.EndsWith("?location", StringComparison.OrdinalIgnoreCase)
|
|
&& response.Request.Method.Equals(HttpMethod.Get))
|
|
{
|
|
var bucketName = response.Request.RequestUri.PathAndQuery.Split('?')[0];
|
|
BucketRegionCache.Instance.Remove(bucketName);
|
|
throw new BucketNotFoundException(bucketName);
|
|
}
|
|
|
|
var errResponse = Utils.DeserializeXml<ErrorResponse>(response.Content);
|
|
|
|
if (response.StatusCode == HttpStatusCode.Forbidden
|
|
&& (errResponse.Code.Equals("SignatureDoesNotMatch", StringComparison.OrdinalIgnoreCase) ||
|
|
errResponse.Code.Equals("InvalidAccessKeyId", StringComparison.OrdinalIgnoreCase)))
|
|
throw new AuthorizationException(errResponse.Resource, errResponse.BucketName, errResponse.Message);
|
|
|
|
// Handle XML response for Bucket Policy not found case
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.OrdinalIgnoreCase)
|
|
&& response.Request.RequestUri.PathAndQuery.EndsWith("?policy", StringComparison.OrdinalIgnoreCase)
|
|
&& response.Request.Method.Equals(HttpMethod.Get)
|
|
&& string.Equals(errResponse.Code, "NoSuchBucketPolicy", StringComparison.OrdinalIgnoreCase))
|
|
throw new ErrorResponseException(errResponse, response) { XmlError = response.Content };
|
|
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.OrdinalIgnoreCase)
|
|
&& string.Equals(errResponse.Code, "NoSuchBucket", StringComparison.OrdinalIgnoreCase))
|
|
throw new BucketNotFoundException(errResponse.BucketName);
|
|
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.OrdinalIgnoreCase)
|
|
&& errResponse.Code.Equals("MalformedXML", StringComparison.OrdinalIgnoreCase))
|
|
throw new MalFormedXMLException(errResponse.Resource, errResponse.BucketName, errResponse.Message,
|
|
errResponse.Key);
|
|
|
|
if (response.StatusCode.ToString()
|
|
.Contains(nameof(HttpStatusCode.NotImplemented), StringComparison.OrdinalIgnoreCase)
|
|
&& errResponse.Code.Equals("NotImplemented", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
#pragma warning disable MA0025 // Implement the functionality instead of throwing NotImplementedException
|
|
throw new NotImplementedException(errResponse.Message);
|
|
}
|
|
#pragma warning restore MA0025 // Implement the functionality instead of throwing NotImplementedException
|
|
|
|
if (response.StatusCode == HttpStatusCode.BadRequest
|
|
&& errResponse.Code.Equals("InvalidRequest", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
_ = new Dictionary<string, string>(StringComparer.Ordinal) { { "legal-hold", "" } };
|
|
if (response.Request.RequestUri.Query.Contains("legalHold", StringComparison.OrdinalIgnoreCase))
|
|
throw new MissingObjectLockConfigurationException(errResponse.BucketName, errResponse.Message);
|
|
}
|
|
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.OrdinalIgnoreCase)
|
|
&& errResponse.Code.Equals("ObjectLockConfigurationNotFoundError", StringComparison.OrdinalIgnoreCase))
|
|
throw new MissingObjectLockConfigurationException(errResponse.BucketName, errResponse.Message);
|
|
|
|
if (response.StatusCode.ToString().Contains(nameof(HttpStatusCode.NotFound), StringComparison.OrdinalIgnoreCase)
|
|
&& errResponse.Code.Equals("ReplicationConfigurationNotFoundError", StringComparison.OrdinalIgnoreCase))
|
|
throw new MissingBucketReplicationConfigurationException(errResponse.BucketName, errResponse.Message);
|
|
|
|
if (response.StatusCode == HttpStatusCode.Conflict
|
|
&& errResponse.Code.Equals("BucketAlreadyOwnedByYou", StringComparison.OrdinalIgnoreCase))
|
|
throw new ArgumentException("Bucket already owned by you: " + errResponse.BucketName, nameof(response));
|
|
|
|
if (response.StatusCode == HttpStatusCode.PreconditionFailed
|
|
&& errResponse.Code.Equals("PreconditionFailed", StringComparison.OrdinalIgnoreCase))
|
|
throw new PreconditionFailedException("At least one of the pre-conditions you " +
|
|
"specified did not hold for object: \"" + errResponse.Resource +
|
|
"\"");
|
|
|
|
throw new UnexpectedMinioException(errResponse.Message) { Response = errResponse, XmlError = response.Content };
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (!disposedValue)
|
|
{
|
|
if (disposing && Config.DisposeHttpClient)
|
|
Config.HttpClient?.Dispose();
|
|
disposedValue = true;
|
|
}
|
|
}
|
|
}
|