文件管理服务

使用using后不必要使用await fileStream.FlushAsync();,因为内部自带
合并文件,手动创建一个可写的文件流,把内容写入即可
toolName: view_files

status: success

filePath: d:\ceshi\MedicTechServer\MedicTechServer\Controllers\KnowController.cs

toolName: search_by_regex

status: success

query: GenerateKey(|AddPartETag(|GetPartETags(|SetUploadId(|GetUploadId(|UploadIdExists(|ClearAll(|AddFileKey(|GetFileKey(|RemoveUploadId(

toolName: view_files

status: success

filePath: d:\ceshi\MedicTechServer\MedicTechServer\Services\MinioService.cs

知识点:MinIO(S3)分片上传在本项目里的实现(UploadFiles + UploadSubmit)

  • 关联代码
    • 分片上传入口:[KnowController.MultipleFiles](file:///d:/ceshi/MedicTechServer/MedicTechServer/Controllers/KnowController.cs#L60-L138)(非 Windows 分支 L95-L136)
    • 分片合并入口:[KnowController.UploadSubmit](file:///d:/ceshi/MedicTechServer/MedicTechServer/Controllers/KnowController.cs#L140-L250)(非 Windows 分支 L211-L248)
    • MinIO 分片上传实现:[MinioService.ChunkUploadAsync](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L309-L354)
    • 内存缓存(UploadId / PartETags / FileKey):[MinioService 缓存方法](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L544-L625)

1) 分片“存在哪里”?

  • 分片不落在你的 API 服务本机文件系统里,而是上传到 MinIO 服务端
  • 上传过程中,MinIO 会把分片临时存放在其数据目录的:.minio.sys/multipart,控制台一般看不到这些分片对象。
    • 代码里已写明:[MinioService.cs:L309-L312](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L309-L312)
  • 当调用 Complete 合并后,MinIO 会生成最终对象,并清理对应 multipart 临时数据:
    • [MinioService.cs:L335-L337](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L335-L337)

2) 核心设计:用内存缓存“记住分片状态”

本项目用 IMemoryCache 保存一次上传会话所需的 3 类信息(都设置了滑动过期 + 绝对 2 小时过期):

  • 缓存键(会话级 key):upload_{orgId}_{userName}_{fileName}
    • 生成逻辑:[GenerateKey](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L545-L548)
  • UploadId:MinIO 分片会话 id(InitiateMultipartUpload 返回)
    • Set/Get:[SetUploadId/GetUploadId](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L551-L563)
  • FileKey(uniqueKey):实际写入 MinIO 的对象 Key(这里用 Guid + 原文件名 防重名)
    • Set/Get:[AddFileKey/GetFileKey](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L602-L613)
  • PartETags 列表:每个分片上传成功后返回的 {PartNumber, ETag}
    • Add/Get:[AddPartETag/GetPartETags](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L580-L595)

3) 分片上传接口(UploadFiles)的流程与伪代码

对应代码段:[KnowController.cs:L95-L135](file:///d:/ceshi/MedicTechServer/MedicTechServer/Controllers/KnowController.cs#L95-L135)

关键点:

  • currentPart = currentPart + 1; 说明客户端传的分片序号很可能是 0 基,而 S3 的 PartNumber 要求从 1 开始。
  • 只有第 1 片会发起 Initiate 并生成 uniqueKey、uploadId,其余分片全靠缓存取回这两个值继续上传。

伪代码(抽象描述):

UploadFiles(orgId, userName, fileName, currentPartFromClient, chunkFileStream):
    partNumber = currentPartFromClient + 1
    cacheKey = "upload_{orgId}_{userName}_{fileName}"

    if partNumber == 1:
        uniqueKey = "{Guid}_{fileName}"
        cache.set(cacheKey + "_fileName", uniqueKey)

        uploadId = minio.initiateMultipart(bucket="medic-tech", key=uniqueKey)
        cache.set(cacheKey + "_uploadId", uploadId)

    uploadId = cache.get(cacheKey + "_uploadId")
    uniqueKey = cache.get(cacheKey + "_fileName")
    assert uploadId not empty

    etag = minio.uploadPart(
        bucket="medic-tech",
        key=uniqueKey,
        uploadId=uploadId,
        partNumber=partNumber,
        body=chunkFileStream,
        partSize=chunkSize
    )

    cache.append(cacheKey + "_partETags", {partNumber, etag})
    return OK

4) 合并提交接口(UploadSubmit)的流程与伪代码

对应代码段:[KnowController.cs:L211-L248](file:///d:/ceshi/MedicTechServer/MedicTechServer/Controllers/KnowController.cs#L211-L248)

它做了 4 件事:

  1. 从缓存拿到 partETagList + uploadId + fileKey(uniqueKey)
  2. 调 MinIO Complete 合并成最终对象
  3. 把最终对象的 Key 写入数据库 Know.Path
  4. 清理缓存(uploadId/partETags/fileKey)

伪代码:

UploadSubmit(orgId, userName, files[]):
    for each file in files:
        cacheKey = "upload_{orgId}_{userName}_{file.name}"

        partETags = cache.get(cacheKey + "_partETags") or []
        uploadId  = cache.get(cacheKey + "_uploadId")
        fileKey   = cache.get(cacheKey + "_fileName")

        completeResp = minio.completeMultipart(
            bucket="medic-tech",
            key=fileKey,
            uploadId=uploadId,
            partETags=partETags
        )

        db.insert Know {
            Name = file.name,
            Path = completeResp.Key,
            FileSize = completeResp.ContentLength,
            ...
        }

        cache.remove(cacheKey + "_uploadId/_partETags/_fileName")

5) 实战注意点(这套方案最容易踩的坑)

  • 单机没问题,多实例会出问题:IMemoryCache 是“本进程内存”,如果 API 部署成多副本/负载均衡,后续分片可能打到另一台机器,就拿不到 uploadId/partETags,会导致无法合并。
  • 2 小时过期:上传时间过长或中断后重试,缓存过期会导致“UploadId not found in cache”(代码里已抛异常:[KnowController.cs:L120-L123](file:///d:/ceshi/MedicTechServer/MedicTechServer/Controllers/KnowController.cs#L120-L123))。
  • PartETags 顺序:Complete 请求通常要求按 PartNumber 升序更稳妥;你这里的 AddPartETag 是按到达顺序追加,若乱序上传,建议合并前排序(你另一个 Complete 方法里就有排序逻辑:[MinioService.cs:L511-L517](file:///d:/ceshi/MedicTechServer/MedicTechServer/Services/MinioService.cs#L511-L517))。
posted @ 2026-05-03 23:09  爱晒太阳的懒猫。。  阅读(15)  评论(0)    收藏  举报