sdk/Util/OssUtils.cs (480 lines of code) (raw):
/*
* Copyright (C) Alibaba Cloud Computing
* All rights reserved.
*
*/
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using Aliyun.OSS.Common;
using Aliyun.OSS.Common.Communication;
using Aliyun.OSS.Commands;
using Aliyun.OSS.Domain;
using Aliyun.OSS.Properties;
using Aliyun.OSS.Common.Internal;
namespace Aliyun.OSS.Util
{
/// <summary>
/// The equvalent delegate of .Net4.0's System.Func. This is to make this code compatible with .Net 2.0
/// </summary>
public delegate TResult OssFunc<in T, out TResult>(T arg);
/// <summary>
/// The equvalent delegate of .Net 4.0's System.Action.
/// </summary>
public delegate void OssAction();
public delegate void OssAction<in T>(T obj);
/// <summary>
/// Some common utility methods and constants
/// </summary>
public static class OssUtils
{
private const string DefaultBaseChars = "0123456789ABCDEF";
private const string CharsetName = "utf-8";
/// <summary>
/// Max normal file size: 5G
/// </summary>
public const long MaxFileSize = 5 * 1024 * 1024 * 1024L;
/// <summary>
/// Max prefix length
/// </summary>
public const int MaxPrefixStringSize = 1024;
/// <summary>
/// Marker's max length.
/// </summary>
public const int MaxMarkerStringSize = 1024;
/// <summary>
/// Max delimiter length.
/// </summary>
public const int MaxDelimiterStringSize = 1024;
/// <summary>
/// Max keys to return in one call.
/// </summary>
public const int MaxReturnedKeys = 1000;
/// <summary>
/// Max objects to delete in multiple object deletion call.
/// </summary>
public const int DeleteObjectsUpperLimit = 1000;
/// <summary>
/// Max CORS rule count per bucket
/// </summary>
public const int BucketCorsRuleLimit = 10;
/// <summary>
/// Max lifecycle rule count per bucket.
/// </summary>
public const int LifecycleRuleLimit = 1000;
/// <summary>
/// Max object key's length.
/// </summary>
public const int ObjectNameLengthLimit = 1023;
/// <summary>
/// Max part number's upper limit.
/// </summary>
public const int PartNumberUpperLimit = 10000;
/// <summary>
/// Default part size.
/// </summary>
public const long DefaultPartSize = 8 * 1024 * 1024;
/// <summary>
/// Minimal part size in multipart upload or copy.
/// </summary>
public const long PartSizeLowerLimit = 100 * 1024;
/// <summary>
/// Max file path length.
/// </summary>
public const int MaxPathLength = 124;
/// <summary>
/// Min file path
/// </summary>
public const int MinPathLength = 4;
/// <summary>
/// Check if the bucket name is valid,.
/// </summary>
/// <param name="bucketName">bucket name</param>
/// <returns>true:valid bucket name</returns>
public static bool IsBucketNameValid(string bucketName)
{
if (string.IsNullOrEmpty(bucketName))
return false;
const string pattern = "^[a-z0-9][a-z0-9\\-]{1,61}[a-z0-9]$";
var regex = new Regex(pattern);
var m = regex.Match(bucketName);
return m.Success;
}
/// <summary>
/// validates the object key
/// </summary>
/// <param name="key">object key</param>
/// <returns>true:valid object key</returns>
public static bool IsObjectKeyValid(string key)
{
if (string.IsNullOrEmpty(key) || key.StartsWith("/") || key.StartsWith("\\"))
return false;
var byteCount = Encoding.GetEncoding(CharsetName).GetByteCount(key);
return byteCount <= ObjectNameLengthLimit;
}
/// <summary>
/// validates the object key
/// </summary>
/// <param name="key">object key</param>
/// <param name="strict">flag</param>
/// <returns>true:valid object key</returns>
public static bool IsObjectKeyValid(string key, bool strict)
{
if (string.IsNullOrEmpty(key) || key.StartsWith("/") || key.StartsWith("\\"))
return false;
if (strict && key.StartsWith("?"))
return false;
var byteCount = Encoding.GetEncoding(CharsetName).GetByteCount(key);
return byteCount <= ObjectNameLengthLimit;
}
internal static String MakeResourcePath(Uri endpoint, string bucket, string key)
{
String resourcePath = (key == null) ? string.Empty : key;
if (IsIp(endpoint))
{
resourcePath = bucket + "/" + resourcePath;
}
return UrlEncodeKey(resourcePath);
}
internal static Uri MakeBucketEndpoint(Uri endpoint, string bucket, ClientConfiguration conf)
{
var uri = new Uri(endpoint.Scheme + "://"
+ ((bucket != null && !conf.IsCname && !IsIp(endpoint))
? (bucket + "." + endpoint.Host) : endpoint.Host)
+ ((endpoint.Port != 80) ? (":" + endpoint.Port) : ""));
return uri;
}
/// <summary>
/// checks if the endpoint is in IP format.
/// </summary>
/// <param name="endpoint">endpoint to check</param>
/// <returns>true: the endpoint is ip.</returns>
private static bool IsIp(Uri endpoint)
{
return Regex.IsMatch(endpoint.Host, @"^((2[0-4]\d|25[0-5]|[01]?\d\d?)\.){3}(2[0-4]\d|25[0-5]|[01]?\d\d?)$");
}
/// <summary>
/// Applies the Url encoding on the key
/// </summary>
/// <param name="key">the object key to encode</param>
/// <returns>The encoded key</returns>
private static String UrlEncodeKey(String key)
{
const char separator = '/';
var segments = key.Split(separator);
var encodedKey = new StringBuilder();
encodedKey.Append(HttpUtils.EncodeUri(segments[0], CharsetName));
for (var i = 1; i < segments.Length; i++)
encodedKey.Append(separator).Append(HttpUtils.EncodeUri(segments[i], CharsetName));
if (key.EndsWith(separator.ToString()))
{
// String#split ignores trailing empty strings, e.g., "a/b/" will be split as a 2-entries array,
// so we have to append all the trailing slash to the uri.
foreach (var ch in key)
{
if (ch == separator)
encodedKey.Append(separator);
else
break;
}
}
return encodedKey.ToString();
}
/// <summary>
/// Trims quotes in the ETag
/// </summary>
/// <param name="eTag">The Etag to trim</param>
/// <returns>The Etag without the quotes</returns>
public static string TrimQuotes(string eTag)
{
return eTag != null ? eTag.Trim('\"') : null;
}
/// <summary>
/// Compute the MD5 on the input stream with the given size.
/// </summary>
/// <param name="input">The input stream</param>
/// <param name="partSize">the part size---it could be less than the stream size</param>
/// <returns>MD5 digest value</returns>
public static string ComputeContentMd5(Stream input, long partSize)
{
using (var md5Calculator = MD5.Create())
{
long position = input.Position;
var partialStream = new PartialWrapperStream(input, partSize);
var md5Value = md5Calculator.ComputeHash(partialStream);
input.Seek(position, SeekOrigin.Begin);
return Convert.ToBase64String(md5Value);
}
}
/// <summary>
/// Computes the content crc64.
/// </summary>
/// <returns>The content crc64.</returns>
/// <param name="input">Input.</param>
/// <param name="length">stream length</param>
public static string ComputeContentCrc64(Stream input, long length)
{
using(Crc64Stream crcStream = new Crc64Stream(input, null, length))
{
byte[] buffer = new byte[32 * 1024];
int readCount = 0;
while(readCount < length)
{
int read = crcStream.Read(buffer, 0, buffer.Length);
if (read == 0)
{
break;
}
readCount += read;
}
if (crcStream.CalculatedHash == null)
{
crcStream.CalculateHash();
}
if (crcStream.CalculatedHash == null || crcStream.CalculatedHash.Length == 0)
{
return string.Empty;
}
else
{
return BitConverter.ToUInt64(crcStream.CalculatedHash, 0).ToString();
}
}
}
/// <summary>
/// Checks if the webpage url is valid.
/// </summary>
/// <param name="webpage">The wenpage url to check</param>
/// <returns>true: the url is valid.</returns>
public static bool IsWebpageValid(string webpage)
{
const string pageSuffix = ".html";
return !string.IsNullOrEmpty(webpage) && webpage.EndsWith(pageSuffix)
&& webpage.Length > pageSuffix.Length;
}
/// <summary>
/// Checks if the logging prefix is valid.
/// </summary>
/// <param name="loggingPrefix">The logging prefix to check</param>
/// <returns>true:valid logging prefix</returns>
public static bool IsLoggingPrefixValid(string loggingPrefix)
{
if (string.IsNullOrEmpty(loggingPrefix))
return true;
const string pattern = "^[a-zA-Z][a-zA-Z0-9\\-]{0,31}$";
var regex = new Regex(pattern);
var m = regex.Match(loggingPrefix);
return m.Success;
}
internal static string BuildPartCopySource(string bucketName, string objectKey)
{
return "/" + bucketName + "/" + UrlEncodeKey(objectKey);
}
internal static string BuildCopyObjectSource(string bucketName, string objectKey)
{
return "/" + bucketName + "/" + UrlEncodeKey(objectKey);
}
internal static bool IsPartNumberInRange(int? partNumber)
{
return (partNumber.HasValue && partNumber > 0
&& partNumber <= OssUtils.PartNumberUpperLimit);
}
internal static void CheckBucketName(string bucketName)
{
if (string.IsNullOrEmpty(bucketName))
throw new ArgumentException(Resources.ExceptionIfArgumentStringIsNullOrEmpty, "bucketName");
if (!IsBucketNameValid(bucketName))
throw new ArgumentException(OssResources.BucketNameInvalid, "bucketName");
}
internal static void CheckObjectKey(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException(Resources.ExceptionIfArgumentStringIsNullOrEmpty, "key");
if (!IsObjectKeyValid(key))
throw new ArgumentException(OssResources.ObjectKeyInvalid, "key");
}
internal static void CheckObjectKey(string key, bool strict)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentException(Resources.ExceptionIfArgumentStringIsNullOrEmpty, "key");
if (!IsObjectKeyValid(key, strict))
throw new ArgumentException(OssResources.ObjectKeyInvalid, "key");
}
internal static string DetermineOsVersion()
{
try
{
var os = Environment.OSVersion;
return "windows " + os.Version.Major + "." + os.Version.Minor;
}
catch (InvalidOperationException /* ex */)
{
return "Unknown OSVersion";
}
}
internal static string DetermineSystemArchitecture()
{
return (IntPtr.Size == 8) ? "x86_64" : "x86";
}
internal static string JoinETag(IEnumerable<string> etags)
{
StringBuilder result = new StringBuilder();
var first = true;
foreach (var etag in etags)
{
if (!first)
result.Append(", ");
result.Append(etag);
first = false;
}
return result.ToString();
}
internal static ClientConfiguration GetClientConfiguration(IServiceClient serviceClient)
{
var outerClient = (RetryableServiceClient) serviceClient;
var innerClient = (ServiceClient)outerClient.InnerServiceClient();
return innerClient.Configuration;
}
internal static IAsyncResult BeginOperationHelper<TCommand>(TCommand cmd, AsyncCallback callback, Object state)
where TCommand : OssCommand
{
var retryableAsyncResult = cmd.AsyncExecute(callback, state) as RetryableAsyncResult;
if (retryableAsyncResult == null)
{
throw new ArgumentException("retryableAsyncResult should not be null");
}
return retryableAsyncResult.InnerAsyncResult;
}
internal static TResult EndOperationHelper<TResult>(IServiceClient serviceClient, IAsyncResult asyncResult)
{
var response = EndOperationHelper(serviceClient, asyncResult);
RetryableAsyncResult retryableAsyncResult = asyncResult as RetryableAsyncResult;
if (retryableAsyncResult == null)
{
retryableAsyncResult = asyncResult.AsyncState as RetryableAsyncResult;
}
if (retryableAsyncResult != null)
{
using (retryableAsyncResult)
{
Debug.Assert(retryableAsyncResult != null);
OssCommand<TResult> cmd = (OssCommand<TResult>)retryableAsyncResult.Context.Command;
return cmd.DeserializeResponse(response);
}
}
else
{
throw new ArgumentException("asyncResult");
}
}
private static ServiceResponse EndOperationHelper(IServiceClient serviceClient, IAsyncResult asyncResult)
{
if (asyncResult == null)
throw new ArgumentNullException("asyncResult");
var retryableAsyncResult = asyncResult as RetryableAsyncResult;
if (retryableAsyncResult == null)
{
ServiceClientImpl.HttpAsyncResult httpAsyncResult =
asyncResult as ServiceClientImpl.HttpAsyncResult;
if (httpAsyncResult == null)
{
throw new ArgumentException("asyncResult");
}
retryableAsyncResult = httpAsyncResult.AsyncState as RetryableAsyncResult;
}
if (retryableAsyncResult != null)
{
ServiceClientImpl.HttpAsyncResult httpAsyncResult =
retryableAsyncResult.InnerAsyncResult as ServiceClientImpl.HttpAsyncResult;
return serviceClient.EndSend(httpAsyncResult);
}
else
{
throw new ArgumentException("asyncResult");
}
}
internal static void CheckCredentials(string accessKeyId, string accessKeySecret)
{
if (string.IsNullOrEmpty(accessKeyId))
throw new ArgumentException(Resources.ExceptionIfArgumentStringIsNullOrEmpty, "accessKeyId");
if (string.IsNullOrEmpty(accessKeySecret))
throw new ArgumentException(Resources.ExceptionIfArgumentStringIsNullOrEmpty, "accessKeySecret");
}
internal static Stream SetupProgressListeners(Stream originalStream,
long progressUpdateInterval,
object sender,
EventHandler<StreamTransferProgressArgs> callback)
{
return SetupProgressListeners(originalStream, originalStream.Length, progressUpdateInterval, sender, callback);
}
internal static Stream SetupProgressListeners(Stream originalStream,
long contentLength,
long progressUpdateInterval,
object sender,
EventHandler<StreamTransferProgressArgs> callback)
{
return SetupProgressListeners(originalStream, contentLength, 0, progressUpdateInterval, sender, callback);
}
/// <summary>
/// Sets up the progress listeners
/// </summary>
/// <param name="originalStream">The content stream</param>
/// <param name="contentLength">The length of originalStream</param>
/// <param name="totalBytesRead">The length which has read</param>
/// <param name="progressUpdateInterval">The interval at which progress needs to be published</param>
/// <param name="sender">The objects which is trigerring the progress changes</param>
/// <param name="callback">The callback which will be invoked when the progress changed event is trigerred</param>
/// <returns>an <see cref="EventStream"/> object, incase the progress is setup, else returns the original stream</returns>
internal static Stream SetupProgressListeners(Stream originalStream,
long contentLength,
long totalBytesRead,
long progressUpdateInterval,
object sender,
EventHandler<StreamTransferProgressArgs> callback)
{
var eventStream = new EventStream(originalStream, true);
var tracker = new StreamReadTracker(sender, callback, contentLength, totalBytesRead, progressUpdateInterval);
eventStream.OnRead += tracker.ReadProgress;
return eventStream;
}
internal static Stream SetupDownloadProgressListeners(Stream originalStream,
long contentLength,
long totalBytesWrite,
long progressUpdateInterval,
object sender,
EventHandler<StreamTransferProgressArgs> callback)
{
var eventStream = new EventStream(originalStream, true);
var tracker = new StreamTransferTracker(sender, callback, contentLength, totalBytesWrite, progressUpdateInterval);
eventStream.OnWrite += tracker.TransferredProgress;
return eventStream;
}
/// <summary>
/// Calls a specific EventHandler in a background thread
/// </summary>
/// <param name="handler"></param>
/// <param name="args"></param>
/// <param name="sender"></param>
internal static void InvokeInBackground<T>(EventHandler<T> handler, T args, object sender) where T : EventArgs
{
if (handler == null) return;
var list = handler.GetInvocationList();
foreach (var call in list)
{
var eventHandler = ((EventHandler<T>)call);
if (eventHandler != null)
{
// TODO: BackgroundInvoker
eventHandler(sender, args);
}
}
}
internal static Uri GetEndpointFromSignedUrl(Uri signedUrl)
{
var uri = new Uri(signedUrl.Scheme + "://" + signedUrl.Authority);
return uri;
}
internal static String GetResourcePathFromSignedUrl(Uri signedUrl)
{
var str = signedUrl.AbsolutePath;
return str.StartsWith("/") ? str.Substring(1) : str;
}
internal static IDictionary<String, String> GetParametersFromSignedUrl(Uri signedUrl)
{
var parameters = new Dictionary<String, String>();
var query = signedUrl.Query;
int index = 0;
if (query.Length > 0 && query[0] == '?')
{
index = 1;
}
var array = query.Substring(index).Split('&');
foreach (var i in array)
{
var param = i.Split('=');
if (param.Length == 2)
{
parameters.Add(HttpUtils.DecodeUri(param[0]), HttpUtils.DecodeUri(param[1]));
}
else
{
parameters.Add(HttpUtils.DecodeUri(param[0]), "");
}
}
return parameters;
}
internal static int ConvertBytesToInt(byte[] data, int start = 0, int size = 4)
{
int ret = 0;
int len = data.Length - start;
len = Math.Min(len, size);
for (int i = 0; i < len; i++)
{
ret <<= 8;
ret |= data[start + i] & 0xFF;
}
return ret;
}
internal static uint ConvertBytesToUint(byte[] data, int start = 0, int size = 4)
{
uint ret = 0;
int len = data.Length - start;
len = Math.Min(len, size);
for (int i = 0; i < len; i++)
{
ret <<= 8;
ret |= data[start + i] & 0xFFU;
}
return ret;
}
internal static long ConvertBytesToLong(byte[] data, int start = 0, int size = 8)
{
long ret = 0;
int len = data.Length - start;
len = Math.Min(len, size);
for (int i = 0; i < len; i++)
{
ret <<= 8;
ret |= (long)(data[start + i] & 0xFF);
}
return ret;
}
internal static string ToHexString(byte[] data, bool lowercase)
{
var sb = new StringBuilder();
for (var i = 0; i < data.Length; i++)
{
sb.Append(data[i].ToString(lowercase ? "x2" : "X2"));
}
return sb.ToString();
}
}
}