[fix]GetNext
大石头 编写于 2024-06-25 16:49:36
X
using System.Net.Http;
using System.Net.Http.Headers;
using System.Reflection;
using System.Text;
using NewLife.Data;
using NewLife.IO;
using NewLife.Log;
using NewLife.Remoting;

namespace NewLife.Yun;

/// <summary>阿里云文件存储</summary>
/// <remarks>
/// 文档 https://newlifex.com/core/oss
/// </remarks>
public class OssClient : IObjectStorage
{
    #region 属性
    /// <summary>访问域名。Endpoint</summary>
    public String? Server { get; set; } = "http://oss-cn-shanghai.aliyuncs.com";

    /// <summary>访问密钥。AccessKeyId</summary>
    public String? AppId { get; set; }

    /// <summary>访问密钥。AccessKeySecret</summary>
    public String? Secret { get; set; }

    /// <summary>存储空间</summary>
    public String? BucketName { get; set; }

    /// <summary>是否支持获取文件直接访问Url</summary>
    public Boolean CanGetUrl => false;

    /// <summary>是否支持删除</summary>
    public Boolean CanDelete => true;

    /// <summary>是否支持搜索</summary>
    public Boolean CanSearch => true;

    private String? _bucketName;
    private String? _baseAddress;
    private HttpClient? _Client;
    #endregion

    #region 远程操作
    private HttpClient GetClient()
    {
        if (_Client != null) return _Client;

        var addr = _baseAddress ?? Server;
        if (addr.IsNullOrEmpty()) throw new ArgumentNullException(nameof(Server), "OSS service address not specified");

        var http = DefaultTracer.Instance.CreateHttpClient();
        http.BaseAddress = new Uri(addr);

        var asm = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
        var asmName = asm?.GetName();
        if (asmName != null && !asmName.Name.IsNullOrEmpty())
        {
            //var userAgent = $"{asmName.Name}/{asmName.Version}({Environment.OSVersion};{Environment.Version})";
            http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(asmName.Name, asmName.Version + ""));
        }

        return _Client = http;
    }

    private void SetBucket(String? bucketName)
    {
        var url = Server;
        if (!bucketName.IsNullOrEmpty() && !url.IsNullOrEmpty())
        {
            var ss = url.Split("://");
            url = $"{ss[0]}://{bucketName}.{ss[1]}";
        }

        // 判断是否有改变
        if (_baseAddress != url)
        {
            _baseAddress = url;
            _Client = null;
        }

        _bucketName = bucketName;
    }

    /// <summary>异步调用命令</summary>
    /// <param name="method"></param>
    /// <param name="action"></param>
    /// <param name="args"></param>
    /// <returns></returns>
    protected async Task<TResult?> InvokeAsync<TResult>(HttpMethod method, String action, Object? args = null)
    {
        var request = ApiHelper.BuildRequest(method, action, args);

        // 资源路径
        var resourcePath = action;
        if (!_bucketName.IsNullOrEmpty()) resourcePath = "/" + _bucketName + resourcePath;

        // 时间
        request.Headers.Date = DateTimeOffset.UtcNow;
        //request.Headers.Add("Date", DateTime.UtcNow.ToString("ddd, dd MMM yyyy HH:mm:ss \\G\\M\\T"));

        // 签名
        var canonicalString = BuildCanonicalString(method.Method, resourcePath, request);
        var signature = canonicalString.GetBytes().SHA1(Secret.GetBytes()).ToBase64();
        request.Headers.Authorization = new AuthenticationHeaderValue("OSS", AppId + ":" + signature);

        var http = GetClient();
        var rs = await http.SendAsync(request);

        return await ApiHelper.ProcessResponse<TResult>(rs);
    }

    private async Task<IDictionary<String, Object?>?> GetAsync(String action, Object? args = null) => await InvokeAsync<IDictionary<String, Object?>>(HttpMethod.Get, action, args);
    #endregion

    #region Bucket操作
    /// <summary>列出所有存储空间名称</summary>
    /// <returns></returns>
    public async Task<String[]?> ListBuckets()
    {
        SetBucket(null);

        var rs = await GetAsync("/");

        var bs = rs?["Buckets"] as IDictionary<String, Object>;
        var bk = bs?["Bucket"];

        if (bk is IList<Object> list) return list.Select(e => (e as IDictionary<String, Object?>)!["Name"] + "").ToArray();
        if (bk is IDictionary<String, Object> dic) return [dic["Name"] + ""];

        return null;
    }

    /// <summary>列出所有存储空间明细,支持过滤</summary>
    /// <param name="prefix"></param>
    /// <param name="marker"></param>
    /// <param name="maxKeys"></param>
    /// <returns></returns>
    public async Task<IList<ObjectInfo>?> ListBuckets(String prefix, String marker, Int32 maxKeys = 100)
    {
        SetBucket(null);

        var rs = await GetAsync("/", new { prefix, marker, maxKeys });

        var bs = rs?["Buckets"] as IDictionary<String, Object>;
        var bk = bs?["Bucket"] as IList<Object>;
        if (bk is not IList<Object> list) return null;

        var infos = new List<ObjectInfo>();
        foreach (var item in list.Cast<IDictionary<String, Object>>())
        {

        }

        return infos;
    }
    #endregion

    #region Object操作
    /// <summary>列出所有文件名称</summary>
    /// <returns></returns>
    public async Task<String[]?> ListObjects()
    {
        SetBucket(BucketName);

        var rs = await GetAsync("/");

        var contents = rs?["Contents"];
        if (contents is IList<Object> list) return list?.Select(e => (e as IDictionary<String, Object?>)!["Key"] + "").ToArray();
        if (contents is IDictionary<String, Object> dic) return [dic["Key"] + ""];

        return null;
    }

    /// <summary>列出所有文件明细,支持过滤</summary>
    /// <param name="prefix"></param>
    /// <param name="marker"></param>
    /// <param name="maxKeys"></param>
    /// <returns></returns>
    public async Task<IList<ObjectInfo>?> ListObjects(String prefix, String marker, Int32 maxKeys = 100)
    {
        SetBucket(BucketName);

        var rs = await GetAsync("/", new { prefix, marker, maxKeys });

        var contents = rs?["Contents"];
        if (contents is not IList<Object> list) return null;

        var infos = new List<ObjectInfo>();
        foreach (var item in list.Cast<IDictionary<String, Object>>())
        {

        }

        return infos;
    }

    /// <summary>上传文件</summary>
    /// <param name="objectName">对象文件名</param>
    /// <param name="data">数据内容</param>
    /// <returns></returns>
    public async Task<IObjectInfo?> Put(String objectName, Packet data)
    {
        SetBucket(BucketName);

        var content = data.Next == null ?
            new ByteArrayContent(data.Data, data.Offset, data.Count) :
            new ByteArrayContent(data.ReadBytes());
        var rs = await InvokeAsync<Packet>(HttpMethod.Put, "/" + objectName, content);

        return new ObjectInfo { Name = objectName, Data = rs };
    }

    /// <summary>获取文件</summary>
    /// <param name="objectName"></param>
    /// <returns></returns>
    public async Task<IObjectInfo?> Get(String objectName)
    {
        SetBucket(BucketName);

        var rs = await InvokeAsync<Packet>(HttpMethod.Get, "/" + objectName);

        return new ObjectInfo { Name = objectName, Data = rs };
    }

    /// <summary>获取文件直接访问Url</summary>
    /// <param name="id">对象文件名</param>
    /// <returns></returns>
    public Task<String?> GetUrl(String id) => throw new NotImplementedException();

    /// <summary>删除文件</summary>
    /// <param name="objectName"></param>
    /// <returns></returns>
    public async Task<Int32> Delete(String objectName)
    {
        SetBucket(BucketName);

        var rs = await InvokeAsync<Object>(HttpMethod.Delete, "/" + objectName);

        return rs != null ? 1 : 0;
    }

    /// <summary>搜索文件</summary>
    /// <param name="pattern">匹配模式。如/202304/*.jpg</param>
    /// <param name="start">开始序号。0开始</param>
    /// <param name="count">最大个数</param>
    /// <returns></returns>
    public Task<IList<IObjectInfo>?> Search(String? pattern, Int32 start, Int32 count) => throw new NotImplementedException();
    #endregion

    #region 辅助
    private const Char NewLineMarker = '\n';

    private static readonly IList<String> ParamtersToSign = new List<String> {
        "acl", "uploadId", "partNumber", "uploads", "cors", "logging",
        "website", "delete", "referer", "lifecycle", "security-token","append",
        "position", "x-oss-process", "restore", "bucketInfo", "stat", "symlink",
        "location", "qos", "policy", "tagging", "requestPayment", "x-oss-traffic-limit",
        "objectMeta", "encryption", "versioning", "versionId", "versions",
        "live", "status", "comp", "vod", "startTime", "endTime",
        "inventory","continuation-token","inventoryId",
        "callback", "callback-var","x-oss-request-payer",
        "worm","wormId","wormExtend",
        "response-cache-control",
        "response-content-disposition",
        "response-content-encoding",
        "response-content-language",
        "response-content-type",
        "response-expires"
    };

    private static String BuildCanonicalString(String method, String resourcePath, HttpRequestMessage request)
    {
        var sb = new StringBuilder();

        sb.Append(method).Append(NewLineMarker);

        var headersToSign = new Dictionary<String, String?>(StringComparer.OrdinalIgnoreCase);
        var headers = request.Headers;
        if (headers != null)
        {
            foreach (var header in headers)
            {
                if (header.Key.EqualIgnoreCase("Content-Type", "Content-MD5", "Date") ||
                    header.Key.StartsWithIgnoreCase("x-oss-"))
                    headersToSign.Add(header.Key, header.Value?.Join());
            }
        }

        if (!headersToSign.ContainsKey("Content-Type")) headersToSign.Add("Content-Type", "");
        if (!headersToSign.ContainsKey("Content-MD5")) headersToSign.Add("Content-MD5", "");

        var sortedHeaders = headersToSign.Keys.OrderBy(e => e).ToList();
        foreach (var key in sortedHeaders)
        {
            var value = headersToSign[key];
            if (key.StartsWithIgnoreCase("x-oss-"))
                sb.Append(key.ToLowerInvariant()).Append(':').Append(value);
            else
                sb.Append(value);

            sb.Append(NewLineMarker);
        }

        sb.Append(resourcePath);

#if NET5_0_OR_GREATER
        var parameters = request.Options;
#else
        var parameters = request.Properties;
#endif
        if (parameters != null)
        {
            var separator = '?';
            foreach (var item in parameters.OrderBy(e => e.Key))
            {
                if (!ParamtersToSign.Contains(item.Key)) continue;

                sb.Append(separator);
                sb.Append(item.Key);
                var paramValue = item.Value;
                if (!String.IsNullOrEmpty(paramValue + ""))
                    sb.Append('=').Append(paramValue);

                separator = '&';
            }
        }

        return sb.ToString();
    }
    #endregion
}