/*
* 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 System.Text;
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;
///
/// Creates and returns an MinIO Client
///
/// Client with no arguments to be used with other builder methods
public MinioClient()
{
}
public MinioConfig Config { get; } = new();
public IEnumerable ResponseErrorHandlers { get; internal set; } =
Enumerable.Empty();
///
/// Default error handling delegate
///
public IApiResponseErrorHandler DefaultErrorHandler { get; internal set; } = new DefaultErrorHandler();
public IRequestLogger RequestLogger { get; internal set; }
///
/// Runs httpClient's GetAsync method
///
public Task WrapperGetAsync(Uri uri)
{
return Config.HttpClient.GetAsync(uri);
}
///
/// Runs httpClient's PutObjectAsync method
///
public Task WrapperPutAsync(Uri uri, StreamContent strm)
{
return Task.Run(async () => await Config.HttpClient.PutAsync(uri, strm).ConfigureAwait(false));
}
///
/// Sets HTTP tracing On.Writes output to Console
///
//public void SetTraceOn(IRequestLogger requestLogger = null)
//{
// var logger = Config?.ServiceProvider?.GetRequiredService>();
// RequestLogger = requestLogger ?? new DefaultRequestLogger(logger);
// Config.TraceHttp = true;
//}
///
/// Sets HTTP tracing Off.
///
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);
}
///
/// Parse response errors if any and return relevant error messages
///
///
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.Equals(response.StatusCode) ||
HttpStatusCode.TemporaryRedirect.Equals(response.StatusCode) ||
HttpStatusCode.MovedPermanently.Equals(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 (HttpStatusCode.Forbidden.Equals(response.StatusCode)
|| HttpStatusCode.BadRequest.Equals(response.StatusCode)
|| HttpStatusCode.NotFound.Equals(response.StatusCode)
|| HttpStatusCode.MethodNotAllowed.Equals(response.StatusCode)
|| HttpStatusCode.NotImplemented.Equals(response.StatusCode))
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);
#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 (HttpStatusCode.NotFound.Equals(response.StatusCode))
{
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, "Not found.");
}
else if (pathLength == 1)
{
var resource = resourceSplits[0];
if (isAWS && isVirtual && !string.IsNullOrEmpty(pathAndQuery))
{
errorResponse.Code = "NoSuchKey";
error = new ObjectNotFoundException(resource, "Not found.");
}
else
{
errorResponse.Code = "NoSuchBucket";
BucketRegionCache.Instance.Remove(resource);
error = new BucketNotFoundException(resource, "Not found.");
}
}
else
{
error = new InternalClientException("404 without body resulted in path with less than two components",
response);
}
}
else if (HttpStatusCode.BadRequest.Equals(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.Equals(response.StatusCode))
{
errorResponse.Code = "Forbidden";
error = new AccessDeniedException("Access denied on the resource: " + pathAndQuery);
}
error.Response = errorResponse;
throw error;
}
private static void ParseErrorFromContent(ResponseResult response)
{
if (response is null)
throw new ArgumentNullException(nameof(response));
if (response.StatusCode.Equals(HttpStatusCode.NotFound)
&& 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, "Not found.");
}
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(response.Content).AsMemory().ToArray());
var errResponse = Utils.DeserializeXml(stream);
if (response.StatusCode.Equals(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.Equals(HttpStatusCode.NotFound)
&& 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.Equals(HttpStatusCode.NotFound)
&& string.Equals(errResponse.Code, "NoSuchBucket", StringComparison.OrdinalIgnoreCase))
throw new BucketNotFoundException(errResponse.BucketName, "Not found.");
if (response.StatusCode.Equals(HttpStatusCode.BadRequest)
&& errResponse.Code.Equals("MalformedXML", StringComparison.OrdinalIgnoreCase))
throw new MalFormedXMLException(errResponse.Resource, errResponse.BucketName, errResponse.Message,
errResponse.Key);
if (response.StatusCode.Equals(HttpStatusCode.NotImplemented)
&& 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.Equals(HttpStatusCode.BadRequest)
&& errResponse.Code.Equals("InvalidRequest", StringComparison.OrdinalIgnoreCase))
{
var legalHold = new Dictionary(StringComparer.Ordinal) { { "legal-hold", "" } };
if (response.Request.RequestUri.Query.Contains("legalHold", StringComparison.OrdinalIgnoreCase))
throw new MissingObjectLockConfigurationException(errResponse.BucketName, errResponse.Message);
}
if (response.StatusCode.Equals(HttpStatusCode.NotFound)
&& errResponse.Code.Equals("ObjectLockConfigurationNotFoundError", StringComparison.OrdinalIgnoreCase))
throw new MissingObjectLockConfigurationException(errResponse.BucketName, errResponse.Message);
if (response.StatusCode.Equals(HttpStatusCode.NotFound)
&& errResponse.Code.Equals("ReplicationConfigurationNotFoundError", StringComparison.OrdinalIgnoreCase))
throw new MissingBucketReplicationConfigurationException(errResponse.BucketName, errResponse.Message);
if (response.StatusCode.Equals(HttpStatusCode.Conflict)
&& errResponse.Code.Equals("BucketAlreadyOwnedByYou", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("Bucket already owned by you: " + errResponse.BucketName,
nameof(response));
throw new UnexpectedMinioException(errResponse.Message) { Response = errResponse, XmlError = response.Content };
}
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
if (Config.DisposeHttpClient)
Config.HttpClient?.Dispose();
disposedValue = true;
}
}
}