Skip to content

文件存储服务

HMX平台支持S3标准的对象存储服务(如RustFS、MinIO、阿里云OSS等),用于存储和管理业务文件。

配置

连接配置格式

http://{accessKey}:{secretKey}@{host}:{port}/{bucket}

示例:

http://admin:admin@192.168.132.86:29000/mes

配置存储方式

方式一:数据库存储(推荐)

通过系统参数表存储,支持动态修改:

csharp
// 从数据库读取配置
var settings = await SF.Proxy<ISystemKeyValueAppService>()
    .GetSysKvListByGroup("A0000:GZMES_SETTINGS");
var rustfs = settings.FirstOrDefault(x => x.CCode == "RUSTFS");

if (rustfs != null && rustfs.CValue.IsNotNullOrEmpty())
{
    _config = ParseRustFsUrl(rustfs.CValue);
}

方式二:配置文件(不推荐)

⚠️ 安全警告:将AccessKey/SecretKey写在配置文件中存在安全风险,客户端程序可能被反编译导致密钥泄露。生产环境请使用数据库存储方式。

json
{
  "S3": {
    "ServiceUrl": "http://192.168.132.86:29000",
    "AccessKey": "admin",
    "SecretKey": "admin",
    "Bucket": "mes"
  }
}

URL解析

csharp
private static S3Config ParseRustFsUrl(string url)
{
    var uri = new Uri(url);
    var accessKey = Uri.UnescapeDataString(uri.UserInfo.Split(':')[0]);
    var secretKey = Uri.UnescapeDataString(uri.UserInfo.Split(':')[1]);
    var serviceUrl = $"{uri.Scheme}://{uri.Host}:{uri.Port}";
    var bucket = uri.AbsolutePath.TrimStart('/');

    return new S3Config
    {
        ServiceUrl = serviceUrl,
        AccessKey = accessKey,
        SecretKey = secretKey,
        LastBucket = bucket
    };
}

核心功能

S3Service 服务层

功能方法说明
连接测试TestConnectionAsync()测试S3服务是否可达
Bucket管理ListBucketsAsync()列出所有Bucket
CreateBucketAsync()创建Bucket
DeleteBucketAsync()删除Bucket
文件列表ListObjectsAsync()列出文件和目录(支持分页)
文件上传UploadFileAsync()上传文件(自动分片)
文件下载DownloadFileAsync()下载文件(带进度)
文件删除DeleteObjectAsync()删除单个文件
DeleteObjectsAsync()批量删除(最多1000个)
文件重命名RenameObjectAsync()复制+删除实现
文件移动MoveObjectAsync()复制+删除实现
目录操作CreateDirectoryAsync()创建虚拟目录
DeleteDirectoryAsync()删除虚拟目录
文件预览GetFileStreamAsync()获取文件流
GetObjectMetadataAsync()获取文件元数据

使用示例

1. 初始化连接

csharp
public class FileStorageService
{
    private readonly S3Service _s3Service = new();

    public void Initialize(string connectionUrl)
    {
        var config = ParseRustFsUrl(connectionUrl);
        _s3Service.Configure(config.ServiceUrl, config.AccessKey, config.SecretKey);
    }
}

2. 上传文件

csharp
// 简单上传(小文件)
await _s3Service.UploadFileAsync(
    bucketName: "mes",
    key: "documents/2024/report.pdf",
    localFilePath: @"C:\temp\report.pdf",
    progress: new Progress<S3TransferProgressInfo>(p =>
    {
        Console.WriteLine($"上传进度: {p.PercentComplete}% ({p.SpeedDisplay})");
    }));

3. 分片上传(大文件)

csharp
// 大文件自动分片(>8MB自动启用)
// 分片大小: 8MB
// 支持断点续传
// 上传失败自动中止
await _s3Service.UploadFileAsync(
    bucketName: "mes",
    key: "videos/tutorial.mp4",
    localFilePath: @"C:\videos\tutorial.mp4",
    progress: progress);

4. 下载文件

csharp
await _s3Service.DownloadFileAsync(
    bucketName: "mes",
    key: "documents/2024/report.pdf",
    localFilePath: @"C:\downloads\report.pdf",
    progress: new Progress<S3TransferProgressInfo>(p =>
    {
        UpdateProgressBar(p.PercentComplete);
    }));

5. 列出文件

csharp
var files = await _s3Service.ListObjectsAsync(
    bucketName: "mes",
    prefix: "documents/2024/");

foreach (var file in files)
{
    if (file.IsDirectory)
        Console.WriteLine($"📁 {file.DisplayName}/");
    else
        Console.WriteLine($"📄 {file.DisplayName} ({file.SizeDisplay})");
}

6. 批量删除

csharp
var keysToDelete = new List<string>
{
    "temp/file1.txt",
    "temp/file2.txt",
    "temp/file3.txt"
};

int deleted = await _s3Service.DeleteObjectsAsync("mes", keysToDelete);
Console.WriteLine($"已删除 {deleted} 个文件");

7. 创建目录

csharp
// S3没有真正的目录,通过零字节对象模拟
await _s3Service.CreateDirectoryAsync("mes", "documents/2024/01/");

大文件分片上传

分片策略

参数说明
最小分片5MBS3协议要求
默认分片8MB本项目默认值
预读缓冲2个分片提高上传效率
超时时间10分钟单次请求超时
重试次数3次自动重试

上传流程

1. 判断文件大小

   ├─ < 8MB ──► 简单上传 (PutObject)

   └─ ≥ 8MB ──► 分片上传

                  ├─ 1. InitiateMultipartUpload

                  ├─ 2. 循环读取分片
                  │     └─ UploadPart (8MB/片)

                  ├─ 3. CompleteMultipartUpload

                  └─ 失败时: AbortMultipartUpload

代码实现

csharp
// 分片上传核心逻辑
private async Task MultipartUploadAsync(...)
{
    // 1. 初始化分片上传
    var initiateResponse = await _client.InitiateMultipartUploadAsync(...);
    var uploadId = initiateResponse.UploadId;

    try
    {
        var partETags = new List<PartETag>();
        var partNumber = 1;

        // 2. 循环上传分片
        while (true)
        {
            var bytesRead = await ReadFullBufferAsync(fs, buffer, ct);
            if (bytesRead == 0) break;

            var partResponse = await _client.UploadPartAsync(new UploadPartRequest
            {
                UploadId = uploadId,
                PartNumber = partNumber,
                PartSize = bytesRead,
                InputStream = partStream
            });

            partETags.Add(new PartETag { PartNumber = partNumber, ETag = partResponse.ETag });
            partNumber++;
        }

        // 3. 完成上传
        await _client.CompleteMultipartUploadAsync(new CompleteMultipartUploadRequest
        {
            UploadId = uploadId,
            PartETags = partETags
        });
    }
    catch (Exception)
    {
        // 4. 失败时中止
        await _client.AbortMultipartUploadAsync(...);
        throw;
    }
}

进度跟踪

进度模型

csharp
public class S3TransferProgressInfo
{
    public long BytesTransferred { get; set; }
    public long TotalBytes { get; set; }
    public double SpeedBytesPerSecond { get; set; }
    public TimeSpan Elapsed { get; set; }

    public int PercentComplete => TotalBytes > 0
        ? (int)(100 * BytesTransferred / TotalBytes)
        : 0;

    public string SpeedDisplay => SpeedBytesPerSecond switch
    {
        > 1024 * 1024 => $"{SpeedBytesPerSecond / 1024 / 1024:F1} MB/s",
        > 1024 => $"{SpeedBytesPerSecond / 1024:F1} KB/s",
        _ => $"{SpeedBytesPerSecond:F0} B/s"
    };
}

使用进度回调

csharp
var progress = new Progress<S3TransferProgressInfo>(info =>
{
    // 更新UI
    progressBar.Value = info.PercentComplete;
    lblSpeed.Text = info.SpeedDisplay;
    lblEta.Text = CalculateEta(info);
});

await _s3Service.UploadFileAsync(bucket, key, filePath, progress, cts.Token);

ContentType映射

扩展名ContentType
.jpg, .jpegimage/jpeg
.pngimage/png
.pdfapplication/pdf
.doc, .docxapplication/msword
.xls, .xlsxapplication/vnd.ms-excel
.mp4video/mp4
.zipapplication/zip
其他application/octet-stream

文件预览类型

csharp
public enum S3PreviewType
{
    Image,      // 图片预览
    Pdf,        // PDF预览
    Office,     // Office文档
    Text,       // 文本文件
    Video,      // 视频播放
    Unsupported // 不支持预览
}

最佳实践

1. Key命名规范

{业务类型}/{年月}/{文件名}
示例:
documents/2024/01/report.pdf
images/products/2024-01-15/logo.png
attachments/order/ORD20240101001/spec.pdf

2. 错误处理

csharp
try
{
    await _s3Service.UploadFileAsync(...);
}
catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound)
{
    // Bucket不存在,创建后重试
    await _s3Service.CreateBucketAsync(bucketName);
    await _s3Service.UploadFileAsync(...);
}
catch (OperationCanceledException)
{
    // 用户取消
    ShowMessage("上传已取消");
}

3. 取消上传

csharp
using var cts = new CancellationTokenSource();

// 绑定取消按钮
btnCancel.Click += (_, _) => cts.Cancel();

await _s3Service.UploadFileAsync(bucket, key, filePath, progress, cts.Token);

4. 批量操作

csharp
// 批量删除(自动分批,每批最多1000个)
var keys = fileList.Select(f => f.Key).ToList();
int deleted = await _s3Service.DeleteObjectsAsync(bucketName, keys);

常见问题

上传超时

原因:文件过大或网络不稳定

解决方案

  • 使用分片上传(已自动启用)
  • 增加超时时间:config.Timeout = TimeSpan.FromMinutes(20)
  • 检查网络连接

连接失败

排查步骤

  1. 确认S3服务地址可访问
  2. 确认AccessKey/SecretKey正确
  3. 确认Bucket存在
  4. 检查防火墙设置

文件名乱码

原因:中文文件名编码问题

解决方案:使用URL编码

csharp
var encodedKey = Uri.EscapeDataString("中文文件名.pdf");

HiMind 工业互联网平台 技术文档