using Dpz.Core.Entity.Base.PublicStruct;
using Dpz.Core.Infrastructure;
using Dpz.Core.Shard.Service;
using FluentFTP;
using Microsoft.Extensions.Logging;

namespace Dpz.Core.Service.ObjectStorage.Services.Impl;

public class ObjectStorageService(
    UpyunOperator upyunOperator,
    HttpClient httpClient,
    ILogger<ObjectStorageService> logger,
    IFtpLogger ftpLogger,
    ParallelChunkBreakpointUpload<UpyunOperator> parallelChunkBreakpointUpload,
    UpyunUpload upyunUpload
) : IObjectStorageOperation
{
    private const string FtpHost = "v0.ftp.upyun.com";

    public string Bucket => upyunOperator.Bucket ?? throw new BusinessException("Bucket is null");

    public async Task<UploadResult> UploadAsync(
        Stream stream,
        ICollection<string> path,
        string filename,
        CancellationToken cancellationToken = default
    )
    {
        return await UploadAsync(
            path,
            filename,
            () => new StreamContent(stream),
            cancellationToken: cancellationToken
        );
    }

    private async Task<UploadResult> UploadAsync(
        IEnumerable<string> path,
        string filename,
        Func<HttpContent> setContent,
        string? contentMd5 = null,
        CancellationToken cancellationToken = default
    )
    {
        if (string.IsNullOrEmpty(filename))
        {
            throw new ArgumentNullException(nameof(filename));
        }
        var pathList = path.ToList();
        pathList.Add(filename);

        return await UploadAsync(pathList, setContent, contentMd5, cancellationToken);
    }

    private async Task<UploadResult> UploadAsync(
        ICollection<string> pathToFile,
        Func<HttpContent> setContent,
        string? contentMd5 = null,
        CancellationToken cancellationToken = default
    )
    {
        return await upyunUpload.UploadAsync(
            pathToFile,
            setContent,
            upyunOperator,
            contentMd5,
            cancellationToken
        );
    }

    public async Task<FileAddress?> UploadFileAsync(
        CloudFile file,
        CancellationToken cancellationToken = default
    )
    {
        return await parallelChunkBreakpointUpload.UploadFileAsync(
            file,
            UploadAsync,
            cancellationToken: cancellationToken
        );
    }

    public async Task<UploadResult> UploadAsync(
        byte[] bytes,
        ICollection<string> path,
        string filename,
        CancellationToken cancellationToken = default
    )
    {
        return await UploadAsync(
            path,
            filename,
            () => new ByteArrayContent(bytes),
            cancellationToken: cancellationToken
        );
    }

    public async Task<Stream> DownloadAsync(
        string pathToFile,
        CancellationToken cancellationToken = default
    )
    {
        var request = new HttpRequestMessage(HttpMethod.Get, $"/{Bucket}/{pathToFile}")
        {
            Version = new Version(2, 0),
        };
        await request.SignatureAsync(upyunOperator, cancellationToken: cancellationToken);
        var response = await httpClient.SendAsync(request, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            logger.LogError("download fail,status code:{StatusCode}", response.StatusCode);
            throw new BusinessException(
                $"download fail,response status code:{response.StatusCode}"
            );
        }

        var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
        return stream;
    }

    public async Task SaveAsAsync(
        string pathToFile,
        string path,
        CancellationToken cancellationToken = default
    )
    {
        if (string.IsNullOrEmpty(pathToFile))
        {
            throw new ArgumentNullException(nameof(pathToFile));
        }

        if (string.IsNullOrEmpty(path))
        {
            throw new ArgumentNullException(nameof(path));
        }

        var stream = await DownloadAsync(pathToFile, cancellationToken);
        await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write);
        await stream.CopyToAsync(fileStream, cancellationToken);
        await fileStream.FlushAsync(cancellationToken);
    }

    public async Task DeleteAsync(string? pathToFile, CancellationToken cancellationToken = default)
    {
        if (string.IsNullOrEmpty(pathToFile))
        {
            throw new ArgumentNullException(nameof(pathToFile));
        }
        if (pathToFile.StartsWith("http", StringComparison.OrdinalIgnoreCase))
        {
            pathToFile = pathToFile.Replace(
                upyunOperator.Host
                    ?? throw new BusinessException(
                        "configuration upyun operator host value is null"
                    ),
                ""
            );
        }
        else
        {
            pathToFile = !pathToFile.StartsWith('/') ? $"/{pathToFile}" : pathToFile;
        }

        var request = new HttpRequestMessage(HttpMethod.Delete, $"/{Bucket}{pathToFile}")
        {
            Version = new Version(2, 0),
        };
        await request.SignatureAsync(upyunOperator, cancellationToken: cancellationToken);
        var response = await httpClient.SendAsync(request, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            logger.LogError("delete fail,status code:{StatusCode}", response.StatusCode);
        }
    }

    public async Task<IList<FolderResult>> GetFolderListAsync(
        string path,
        CancellationToken cancellationToken = default
    )
    {
        if (path == null)
        {
            throw new ArgumentNullException(nameof(path));
        }

        try
        {
            await using var client = new AsyncFtpClient(
                FtpHost,
                $"{upyunOperator.Operator}/{upyunOperator.Bucket}",
                upyunOperator.Password,
                logger: ftpLogger
            );

            await client.Connect(cancellationToken);
            //await client.AutoConnectAsync();

            var list = new List<FolderResult>();
            foreach (var item in await client.GetListing(path, cancellationToken))
            {
                var result = new FolderResult { LastUpdateTime = item.Modified, Name = item.Name };
                if (item.Type == FtpObjectType.Directory)
                {
                    result.FileType = FileType.Directory;
                }
                else
                {
                    result.FileType = FileType.File;
                    result.Size = await client.GetFileSize(item.FullName, token: cancellationToken);
                }

                list.Add(result);
            }

            await client.Disconnect(cancellationToken);
            return list;
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "get folder list fail");
            return new List<FolderResult>();
        }
    }

    public async Task<FileInformation> GetFileInformationAsync(
        string pathToFile,
        CancellationToken cancellationToken = default
    )
    {
        if (string.IsNullOrEmpty(pathToFile))
        {
            throw new ArgumentNullException(nameof(pathToFile));
        }

        pathToFile = !pathToFile.StartsWith('/') ? $"/{pathToFile}" : pathToFile;

        var request = new HttpRequestMessage(HttpMethod.Head, $"/{Bucket}{pathToFile}")
        {
            Version = new Version(2, 0),
        };
        await request.SignatureAsync(upyunOperator, cancellationToken: cancellationToken);
        var response = await httpClient.SendAsync(request, cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            logger.LogError("delete fail,status code:{StatusCode}", response.StatusCode);
            throw new BusinessException($"delete fail,response status code:{response.StatusCode}");
        }

        // response.Headers.Contains("x-upyun-frames")
        //     ? response.Headers.GetValues("x-upyun-file-size").FirstOrDefault() ?? "0"
        //     : "0";
        response.Headers.TryGetValues("x-upyun-file-size", out var size);
        long.TryParse(size?.FirstOrDefault() ?? "0", out var length);

        response.Headers.TryGetValues("x-upyun-file-date", out var date);

        long.TryParse(date?.FirstOrDefault() ?? "0", out var timespan);

        var dateTime = timespan.ToDateTime();

        response.Headers.TryGetValues("Content-Md5", out var md5);

        var information = new FileInformation
        {
            Length = length,
            Md5 = md5?.FirstOrDefault() ?? "",
            UploadTime = dateTime,
        };

        return information;
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个对象存储服务的实现类,主要用于与又拍云(UPYUN)存储服务进行交互。让我详细解释一下这个代码:

类概述

ObjectStorageService 实现了 IObjectStorageOperation 接口,提供了完整的对象存储操作功能。

构造函数依赖注入

通过主构造函数注入了以下依赖:

  • UpyunOperator: 又拍云操作器,包含认证信息
  • HttpClient: HTTP客户端,用于API调用
  • ILogger: 日志记录器
  • IFtpLogger: FTP日志记录器
  • ParallelChunkBreakpointUpload: 并行分块断点上传服务
  • UpyunUpload: 又拍云上传服务

主要功能方法

1. 上传功能

  • UploadAsync(Stream): 上传流数据
  • UploadAsync(byte[]): 上传字节数组
  • UploadFileAsync(CloudFile): 上传云文件,支持断点续传
  • 多个重载的私有 UploadAsync 方法处理不同的上传场景

2. 下载功能

  • DownloadAsync(string): 下载文件并返回流
  • SaveAsAsync(string, string): 下载文件并保存到本地路径

3. 删除功能

  • DeleteAsync(string): 删除指定路径的文件
  • 支持处理完整URL和相对路径两种格式

4. 文件管理功能

  • GetFolderListAsync(string): 获取目录列表,使用FTP协议
  • GetFileInformationAsync(string): 获取文件信息(大小、MD5、上传时间等)

技术特点

1. HTTP/2 支持

Version = new Version(2, 0)

所有HTTP请求都使用HTTP/2协议。

2. 签名认证

await request.SignatureAsync(upyunOperator, cancellationToken: cancellationToken);

每个请求都会进行签名认证。

3. 异步编程

所有方法都采用异步模式,支持 CancellationToken

4. 错误处理

  • 参数验证(空值检查)
  • HTTP状态码检查
  • 异常日志记录
  • 业务异常抛出

5. FTP协议支持

在获取文件夹列表时使用FTP协议,这是又拍云提供的一种访问方式。

6. 灵活的路径处理

支持多种路径格式,自动处理URL和相对路径的转换。

使用场景

这个服务适用于需要与又拍云存储进行交互的应用程序,提供了完整的文件上传、下载、删除和管理功能,特别适合需要大文件上传(支持断点续传)的场景。

评论加载中...