Bladeren bron

1.FileMeta 中添加一个 active 字段
2.OssRouterHandler 中处理 PUT 文件上传

reghao 3 weken geleden
bovenliggende
commit
5a006590f4
27 gewijzigde bestanden met toevoegingen van 563 en 370 verwijderingen
  1. 0 36
      oss-api/src/main/java/cn/reghao/oss/api/dto/UploadFilePart.java
  2. 0 1
      oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java
  3. 1 1
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/FastUploadResult.java
  4. 1 5
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadDoneResult.java
  5. 2 1
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFilePart.java
  6. 4 2
      oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadResult.java
  7. 5 3
      oss-api/src/main/java/cn/reghao/oss/api/iface/ConsoleService.java
  8. 1 2
      oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java
  9. 6 6
      oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/OssSdkController.java
  10. 1 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/FileMetaMapper.java
  11. 30 1
      oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ObjectRepository.java
  12. 1 1
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/DataBlock.java
  13. 9 3
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/FileMeta.java
  14. 17 2
      oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadTask.java
  15. 9 17
      oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/ConsoleServiceImpl.java
  16. 0 126
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java
  17. 46 52
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/OssClientService.java
  18. 125 0
      oss-mgr/src/main/java/cn/reghao/oss/mgr/service/PartUploadService.java
  19. 9 4
      oss-mgr/src/main/resources/mapper/FileMetaMapper.xml
  20. 97 15
      oss-sdk/src/main/java/cn/reghao/oss/sdk/OssClient.java
  21. 18 15
      oss-store/src/main/java/cn/reghao/oss/store/disk/HddFlushService.java
  22. 1 1
      oss-store/src/main/java/cn/reghao/oss/store/disk/MetadataCompensator.java
  23. 14 31
      oss-store/src/main/java/cn/reghao/oss/store/handler/OssMultipartUploadHandler.java
  24. 28 34
      oss-store/src/main/java/cn/reghao/oss/store/handler/OssRouterHandler.java
  25. 15 8
      oss-store/src/main/java/cn/reghao/oss/store/handler/OssUploadHandler.java
  26. 123 2
      oss-store/src/main/java/cn/reghao/oss/store/rpc/StoreServiceImpl.java
  27. 0 1
      oss-store/src/main/java/cn/reghao/oss/store/util/UploadState.java

+ 0 - 36
oss-api/src/main/java/cn/reghao/oss/api/dto/UploadFilePart.java

@@ -1,36 +0,0 @@
-package cn.reghao.oss.api.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Getter;
-import lombok.NoArgsConstructor;
-import lombok.Setter;
-
-import java.io.Serializable;
-
-/**
- * @author reghao
- * @date 2022-04-25 10:42:38
- */
-@NoArgsConstructor
-@AllArgsConstructor
-@Getter
-@Setter
-public class UploadFilePart implements Serializable {
-    private static final long serialVersionUID = 1L;
-
-    private int channelCode;
-    // 文件标识
-    private String identifier;
-    private String filename;
-    private String relativePath;
-    // 文件大小
-    private long totalSize;
-    // 分片文件大小
-    private long chunkSize;
-    // 分片文件数量
-    private long totalChunks;
-    // 当前分片文件索引
-    private int chunkNumber;
-    // 当前分片文件大小
-    private int currentChunkSize;
-}

+ 0 - 1
oss-api/src/main/java/cn/reghao/oss/api/dto/media/VideoInfo.java

@@ -30,7 +30,6 @@ public class VideoInfo implements Serializable {
     private String url;
     private String quality;
     private Long size;
-    //private String videoFileId;
     //private LocalDateTime createTime;
 
     public VideoInfo(String objectId, MediaProps mediaProps) {

+ 1 - 1
oss-api/src/main/java/cn/reghao/oss/api/dto/FastUploadResult.java → oss-api/src/main/java/cn/reghao/oss/api/dto/rest/FastUploadResult.java

@@ -1,4 +1,4 @@
-package cn.reghao.oss.api.dto;
+package cn.reghao.oss.api.dto.rest;
 
 import lombok.AllArgsConstructor;
 import lombok.Getter;

+ 1 - 5
oss-api/src/main/java/cn/reghao/oss/api/dto/UploadDoneResult.java → oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadDoneResult.java

@@ -1,7 +1,5 @@
-package cn.reghao.oss.api.dto;
+package cn.reghao.oss.api.dto.rest;
 
-import cn.reghao.oss.api.constant.UploadStatus;
-import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
@@ -12,7 +10,6 @@ import java.io.Serializable;
  * @date 2026-04-30 11:13:40
  */
 @NoArgsConstructor
-@AllArgsConstructor
 @Data
 public class UploadDoneResult implements Serializable {
     private static final long serialVersionUID = 1L;
@@ -22,7 +19,6 @@ public class UploadDoneResult implements Serializable {
     private String absolutePath;
     private long size;
     private String hostPort;
-    private int uploadStatus = UploadStatus.AVAILABLE.getCode();
 
     public UploadDoneResult(String objectId, String sha256sum, String absolutePath, long size, String hostPort) {
         this.objectId = objectId;

+ 2 - 1
oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadFilePart.java

@@ -18,7 +18,8 @@ import java.io.Serializable;
 public class UploadFilePart implements Serializable {
     private static final long serialVersionUID = 1L;
 
-    private int channelCode;
+    private String clientSha256sum;
+    private String uploadId;
     // 文件标识
     private String identifier;
     private String filename;

+ 4 - 2
oss-api/src/main/java/cn/reghao/oss/api/dto/UploadResult.java → oss-api/src/main/java/cn/reghao/oss/api/dto/rest/UploadResult.java

@@ -1,4 +1,4 @@
-package cn.reghao.oss.api.dto;
+package cn.reghao.oss.api.dto.rest;
 
 import lombok.AllArgsConstructor;
 import lombok.Data;
@@ -25,6 +25,8 @@ public class UploadResult implements Serializable {
     private String objectId;
     private String objectName;
     private long uploadBy;
-    private String hostPort;
+    private String host;
+    private int port;
     private int uploadStatus;
+    private boolean active = false;
 }

+ 5 - 3
oss-api/src/main/java/cn/reghao/oss/api/iface/ConsoleService.java

@@ -1,8 +1,10 @@
 package cn.reghao.oss.api.iface;
 
-import cn.reghao.oss.api.dto.*;
-import cn.reghao.oss.api.dto.media.ImageInfo;
-import cn.reghao.oss.api.dto.media.VideoInfo;
+import cn.reghao.oss.api.dto.ObjectMeta;
+import cn.reghao.oss.api.dto.StoreNodeDto;
+import cn.reghao.oss.api.dto.rest.FastUploadResult;
+import cn.reghao.oss.api.dto.rest.UploadDoneResult;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 
 /**
  * 获取由 oss-console 管理的 StoreNode 配置

+ 1 - 2
oss-api/src/main/java/cn/reghao/oss/api/iface/StoreService.java

@@ -3,7 +3,6 @@ package cn.reghao.oss.api.iface;
 import cn.reghao.jutil.jdk.media.model.MediaProps;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
 import cn.reghao.oss.api.dto.media.ImageInfo;
-import cn.reghao.oss.api.dto.media.VideoInfo;
 
 import java.util.List;
 
@@ -18,6 +17,6 @@ public interface StoreService {
     List<DiskVolume> getDiskVolumes();
     String getPartialMd5(String absolutePath, long offset, int length);
     void deleteFile(String absolutePath);
-    MediaProps getMediaInfo(String objectId, String absolutePath);
+    MediaProps getMediaInfo(String objectId, String sha256sum);
     ImageInfo getImageInfo(String objectId, String absolutePath);
 }

+ 6 - 6
oss-mgr/src/main/java/cn/reghao/oss/mgr/controller/OssSdkController.java

@@ -13,7 +13,7 @@ import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
 import cn.reghao.oss.mgr.model.dto.FileInitRequest;
 import cn.reghao.oss.api.dto.rest.UploadSample;
 import cn.reghao.oss.mgr.model.po.StoreNode;
-import cn.reghao.oss.mgr.service.MetadataService;
+import cn.reghao.oss.mgr.service.PartUploadService;
 import cn.reghao.oss.mgr.service.OssClientService;
 import cn.reghao.oss.mgr.service.UserNodeService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -31,13 +31,13 @@ import org.springframework.web.bind.annotation.*;
 @RequestMapping("/api/oss/sdk")
 public class OssSdkController {
     private final OssClientService ossClientService;
-    private final MetadataService metadataService;
+    private final PartUploadService partUploadService;
     private final UserNodeService userNodeService;
 
-    public OssSdkController(OssClientService ossClientService, MetadataService metadataService,
+    public OssSdkController(OssClientService ossClientService, PartUploadService partUploadService,
                             UserNodeService userNodeService) {
         this.ossClientService = ossClientService;
-        this.metadataService = metadataService;
+        this.partUploadService = partUploadService;
         this.userNodeService = userNodeService;
     }
 
@@ -57,14 +57,14 @@ public class OssSdkController {
     @Operation(summary = "对象上传前的预检", description = "N")
     @PostMapping(value = "/prepare", produces = MediaType.APPLICATION_JSON_VALUE)
     public String uploadPrepare(@RequestBody @Validated UploadPrepare uploadPrepare) {
-        UploadPrepareRet uploadPrepareRet = metadataService.prepareUpload(uploadPrepare);
+        UploadPrepareRet uploadPrepareRet = partUploadService.prepareUpload(uploadPrepare);
         return WebResult.success(uploadPrepareRet);
     }
 
     @Operation(summary = "对象快传的二次检查", description = "N")
     @PostMapping(value = "/check_sample", produces = MediaType.APPLICATION_JSON_VALUE)
     public String checkSample(@RequestBody @Validated UploadSample uploadSample) {
-        UploadFileRet uploadFileRet = metadataService.verifyPartialHash(uploadSample);
+        UploadFileRet uploadFileRet = partUploadService.verifyPartialHash(uploadSample);
         return WebResult.success(uploadFileRet);
     }
 

+ 1 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/mapper/FileMetaMapper.java

@@ -18,6 +18,7 @@ public interface FileMetaMapper extends BaseMapper<FileMeta> {
     void updateSha256sum(@Param("objectId") String objectId, @Param("sha256sum") String sha256sum);
     void updateScopeByObjectName(@Param("scope") int scope, @Param("objectName") String objectName);
     void updateScopeByObjectId(@Param("objectId") String objectId, @Param("scope") int scope);
+    void updateSetActiveByObjectId(@Param("objectId") String objectId);
 
     List<FileMeta> findBySha256sum(String sha256sum);
     FileMeta findByObjectId(String objectId);

+ 30 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/db/repository/ObjectRepository.java

@@ -2,10 +2,13 @@ package cn.reghao.oss.mgr.db.repository;
 
 import cn.reghao.oss.api.dto.ObjectInfo;
 import cn.reghao.oss.api.dto.ObjectMeta;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
 import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
+import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
 import cn.reghao.oss.mgr.model.po.DataBlock;
 import cn.reghao.oss.mgr.model.po.FileMeta;
+import cn.reghao.oss.mgr.model.po.UploadTask;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.Cacheable;
@@ -23,22 +26,48 @@ import java.util.List;
 public class ObjectRepository {
     private final FileMetaMapper fileMetaMapper;
     private final DataBlockMapper dataBlockMapper;
+    private UploadTaskMapper uploadTaskMapper;
 
-    public ObjectRepository(FileMetaMapper fileMetaMapper, DataBlockMapper dataBlockMapper) {
+    public ObjectRepository(FileMetaMapper fileMetaMapper, DataBlockMapper dataBlockMapper,
+                            UploadTaskMapper uploadTaskMapper) {
         this.fileMetaMapper = fileMetaMapper;
         this.dataBlockMapper = dataBlockMapper;
+        this.uploadTaskMapper = uploadTaskMapper;
     }
 
     public void saveFileMeta(FileMeta fileMeta) {
         fileMetaMapper.save(fileMeta);
     }
 
+    @Transactional(rollbackFor = Exception.class)
+    public void saveFileMeta(FileMeta fileMeta, UploadResult uploadResult) {
+        String uploadId = uploadResult.getObjectId();
+        int status = uploadResult.getUploadStatus();
+        String host = uploadResult.getHost();
+        int port = uploadResult.getPort();
+        UploadTask uploadTask = uploadTaskMapper.findByUploadId(uploadId);
+        if (uploadTask == null) {
+            uploadTask = new UploadTask(uploadResult);
+            uploadTaskMapper.save(uploadTask);
+        } else {
+            uploadTaskMapper.updateTask(uploadId, status, host, port);
+        }
+
+        fileMetaMapper.save(fileMeta);
+    }
+
     @Transactional(rollbackFor = Exception.class)
     public void saveObject(FileMeta fileMeta, List<DataBlock> list) {
         fileMetaMapper.save(fileMeta);
         dataBlockMapper.saveAll(list);
     }
 
+    @Transactional(rollbackFor = Exception.class)
+    public void saveFastUpload(FileMeta fileMeta, UploadTask uploadTask) {
+        fileMetaMapper.save(fileMeta);
+        uploadTaskMapper.updateTask(uploadTask.getUploadId(), uploadTask.getStatus(), uploadTask.getHost(), uploadTask.getHttpPort());
+    }
+
     @CacheEvict(cacheNames = "oss:store:objectMeta", key = "#objectName")
     public void updateObjectScope(int scope, String objectName) {
         fileMetaMapper.updateScopeByObjectName(scope, objectName);

+ 1 - 1
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/DataBlock.java

@@ -1,7 +1,7 @@
 package cn.reghao.oss.mgr.model.po;
 
 import cn.reghao.jutil.jdk.web.db.BaseObject;
-import cn.reghao.oss.api.dto.UploadDoneResult;
+import cn.reghao.oss.api.dto.rest.UploadDoneResult;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;

+ 9 - 3
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/FileMeta.java

@@ -2,8 +2,8 @@ package cn.reghao.oss.mgr.model.po;
 
 import cn.reghao.jutil.jdk.web.db.BaseObject;
 import cn.reghao.oss.api.constant.ObjectScope;
-import cn.reghao.oss.api.dto.FastUploadResult;
-import cn.reghao.oss.api.dto.UploadResult;
+import cn.reghao.oss.api.dto.rest.FastUploadResult;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
 import lombok.Setter;
@@ -45,9 +45,11 @@ public class FileMeta extends BaseObject<Integer> {
     @Size(max = 255)
     private String contentType;
     @NotNull
+    private Long uploadBy;
+    @NotNull
     private Integer scope;
     @NotNull
-    private Long uploadBy;
+    private Boolean active;
 
     // 目录对象
     public FileMeta(long owner, String objectName, String objectId, String filename, String pid, int scope) {
@@ -61,6 +63,7 @@ public class FileMeta extends BaseObject<Integer> {
         this.pid = pid;
         this.uploadBy = owner;
         this.scope = scope;
+        this.active = false;
     }
 
     public FileMeta(UploadResult uploadResult, int fileType, ObjectScope objectScope) {
@@ -74,6 +77,7 @@ public class FileMeta extends BaseObject<Integer> {
         this.pid = "0";
         this.uploadBy = uploadResult.getUploadBy();
         this.scope = objectScope.getCode();
+        this.active = uploadResult.isActive();
     }
 
     public FileMeta(FastUploadResult fastUploadResult, FileMeta fileMeta, ObjectScope objectScope) {
@@ -87,6 +91,7 @@ public class FileMeta extends BaseObject<Integer> {
         this.pid = "0";
         this.uploadBy = fastUploadResult.getUploadBy();
         this.scope = objectScope.getCode();
+        this.active = false;
     }
 
     public FileMeta(FileMeta fileMeta, String objectId, String objectName, String filename, long uploadBy, int scope) {
@@ -100,5 +105,6 @@ public class FileMeta extends BaseObject<Integer> {
         this.pid = "0";
         this.uploadBy = uploadBy;
         this.scope = scope;
+        this.active = false;
     }
 }

+ 17 - 2
oss-mgr/src/main/java/cn/reghao/oss/mgr/model/po/UploadTask.java

@@ -2,6 +2,7 @@ package cn.reghao.oss.mgr.model.po;
 
 import cn.reghao.jutil.jdk.web.db.BaseObject;
 import cn.reghao.oss.api.dto.rest.UploadPrepare;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 import cn.reghao.oss.mgr.config.UserContext;
 import lombok.Getter;
 import lombok.NoArgsConstructor;
@@ -51,7 +52,7 @@ public class UploadTask extends BaseObject<Integer> {
     private String host;
     private int httpPort;
 
-    public UploadTask(String uploadId, UploadPrepare uploadPrepare, long offset, int length, int splitSize, LocalDateTime expireTime) {
+    public UploadTask(String uploadId, UploadPrepare uploadPrepare, long offset, int length, int splitSize) {
         this.uploadId = uploadId;
         this.sha256sum = uploadPrepare.getSha256sum();
         this.offset = offset;
@@ -60,8 +61,22 @@ public class UploadTask extends BaseObject<Integer> {
         this.filename = uploadPrepare.getFilename();
         this.fileSize = uploadPrepare.getSize();
         this.status = 1;
-        this.expireTime = expireTime;
+        this.expireTime = LocalDateTime.now().plusHours(24);
         this.channelCode = uploadPrepare.getChannelCode();
         this.uploadBy = UserContext.getUserId();
     }
+
+    public UploadTask(UploadResult uploadResult) {
+        this.uploadId = uploadResult.getObjectId();
+        this.sha256sum = uploadResult.getSha256sum();
+        this.offset = 0L;
+        this.length = 0;
+        this.splitSize = 0;
+        this.filename = uploadResult.getFilename();
+        this.fileSize = uploadResult.getSize();
+        this.status = uploadResult.getUploadStatus();
+        this.expireTime = LocalDateTime.now().plusHours(24);
+        this.channelCode = 0;
+        this.uploadBy = uploadResult.getUploadBy();
+    }
 }

+ 9 - 17
oss-mgr/src/main/java/cn/reghao/oss/mgr/rpc/ConsoleServiceImpl.java

@@ -2,8 +2,13 @@ package cn.reghao.oss.mgr.rpc;
 
 import cn.reghao.oss.api.constant.ObjectScope;
 import cn.reghao.oss.api.constant.ObjectType;
-import cn.reghao.oss.api.dto.*;
+import cn.reghao.oss.api.constant.UploadStatus;
 import cn.reghao.oss.api.iface.ConsoleService;
+import cn.reghao.oss.api.dto.ObjectMeta;
+import cn.reghao.oss.api.dto.StoreNodeDto;
+import cn.reghao.oss.api.dto.rest.FastUploadResult;
+import cn.reghao.oss.api.dto.rest.UploadDoneResult;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
 import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
 import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
@@ -71,14 +76,6 @@ public class ConsoleServiceImpl implements ConsoleService {
 
     @Override
     public void registerAndBind(UploadResult uploadResult) {
-        String uploadId = uploadResult.getObjectId();
-        int status = uploadResult.getUploadStatus();
-        String hostPort = uploadResult.getHostPort();
-        String[] arr = hostPort.split(":");
-        String host = arr[0];
-        int port = Integer.parseInt(arr[1]);
-        uploadTaskMapper.updateTask(uploadId, status, host, port);
-
         String contentType = uploadResult.getContentType();
         int fileType = ObjectType.Any.getValue();
         if (contentType.startsWith("video")) {
@@ -95,9 +92,8 @@ public class ConsoleServiceImpl implements ConsoleService {
         String channelPrefix = uploadResult.getChannelPrefix();
         UploadChannel uploadChannel = uploadChannelService.getByChannelPrefixAndCreateBy(channelPrefix, uploadBy);
         ObjectScope objectScope = ObjectScope.getByCode(uploadChannel.getScope());
-
         FileMeta fileMeta = new FileMeta(uploadResult, fileType, objectScope);
-        fileMetaMapper.save(fileMeta);
+        objectRepository.saveFileMeta(fileMeta, uploadResult);
     }
 
     @Override
@@ -111,7 +107,6 @@ public class ConsoleServiceImpl implements ConsoleService {
             return;
         }
 
-        fileMetaMapper.updateSha256sum(objectId, sha256sum);
         DataBlock dataBlock = new DataBlock(uploadDoneResult);
         dataBlockMapper.save(dataBlock);
 
@@ -119,16 +114,13 @@ public class ConsoleServiceImpl implements ConsoleService {
         if (uploadTask == null || !uploadTask.getSha256sum().equals(sha256sum)) {
             return;
         }
-
-        int status = uploadDoneResult.getUploadStatus();
-        uploadTaskMapper.updateStatus(uploadId, status);
+        uploadTaskMapper.updateStatus(uploadId, UploadStatus.AVAILABLE.getCode());
     }
 
     @Override
     public ObjectMeta getObjectMeta(String httpHost, String objectName) {
         UserNode userNode = userNodeService.getUserNodeByDomain(httpHost);
         long uploadBy = userNode.getCreateBy();
-        ObjectMeta result = objectRepository.getObjectMetaByName(objectName, uploadBy);
-        return result;
+        return objectRepository.getObjectMetaByName(objectName, uploadBy);
     }
 }

+ 0 - 126
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/MetadataService.java

@@ -1,126 +0,0 @@
-package cn.reghao.oss.mgr.service;
-
-import cn.reghao.oss.api.dto.rest.UploadFileRet;
-import cn.reghao.oss.api.dto.rest.UploadPrepare;
-import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
-import cn.reghao.oss.api.iface.StoreService;
-import cn.reghao.oss.mgr.config.UserContext;
-import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
-import cn.reghao.oss.mgr.db.mapper.StoreNodeMapper;
-import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
-import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
-import cn.reghao.oss.mgr.db.repository.ObjectRepository;
-import cn.reghao.oss.api.dto.rest.UploadSample;
-import cn.reghao.oss.mgr.model.po.*;
-import cn.reghao.oss.mgr.rpc.RpcService;
-import cn.reghao.oss.mgr.util.StringUtil;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Random;
-import java.util.UUID;
-
-/**
- * @author reghao
- * @date 2026-02-01 15:49:10
- */
-@Slf4j
-@Service
-public class MetadataService {
-    private static final int CHUNK_SIZE = 1024 * 1024; // 采样块大小:1MB
-    private static final int SPLIT_SIZE = 1024 * 1024 * 10; // 分片大小:10MB
-    private final ObjectRepository objectRepository;
-    private StoreNodeMapper storeNodeMapper;
-    private UploadTaskMapper uploadTaskMapper;
-    private RpcService rpcService;
-    private FileMetaMapper fileMetaMapper;
-    private UploadChannelMapper uploadChannelMapper;
-
-    public MetadataService(ObjectRepository objectRepository, StoreNodeMapper storeNodeMapper,
-                           UploadTaskMapper uploadTaskMapper, RpcService rpcService, FileMetaMapper fileMetaMapper,
-                           UploadChannelMapper uploadChannelMapper) {
-        this.objectRepository = objectRepository;
-        this.storeNodeMapper = storeNodeMapper;
-        this.uploadTaskMapper = uploadTaskMapper;
-        this.rpcService = rpcService;
-        this.fileMetaMapper = fileMetaMapper;
-        this.uploadChannelMapper = uploadChannelMapper;
-    }
-
-    public UploadPrepareRet prepareUpload(UploadPrepare uploadPrepare) {
-        String uploadId = UUID.randomUUID().toString().replace("-", "");
-        long size = uploadPrepare.getSize();
-        String sha256sum = uploadPrepare.getSha256sum();
-        DataBlock dataBlock = objectRepository.getBySha256sum(sha256sum);
-        UploadPrepareRet uploadPrepareRet;
-        long randomOffset = 0;
-        if (dataBlock != null) {
-            if (size > CHUNK_SIZE) {
-                Random random = new Random(size);
-                randomOffset = (long) (random.nextDouble() * (size - CHUNK_SIZE));
-            }
-
-            boolean exist = true;
-            uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE, exist, randomOffset, CHUNK_SIZE);
-        } else {
-            long splitNumber = size/SPLIT_SIZE + (size%SPLIT_SIZE != 0 ? 1 : 0);
-            uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE);
-        }
-
-        LocalDateTime expireTime = LocalDateTime.now().plusHours(24);
-        UploadTask uploadTask = new UploadTask(uploadId, uploadPrepare, randomOffset, CHUNK_SIZE, SPLIT_SIZE, expireTime);
-        uploadTaskMapper.save(uploadTask);
-        return uploadPrepareRet;
-    }
-
-    public UploadFileRet verifyPartialHash(UploadSample uploadSample) {
-        String uploadId = uploadSample.getUploadId();
-        String clientPartMd5 = uploadSample.getSampleMd5();
-
-        UploadTask uploadTask = uploadTaskMapper.findByUploadId(uploadId);
-        boolean result = false;
-        if (uploadTask == null) {
-            return new UploadFileRet(uploadId, result);
-        }
-
-        long offset = uploadTask.getOffset();
-        int length = uploadTask.getLength();
-        String sha256sum = uploadTask.getSha256sum();
-        DataBlock dataBlock = objectRepository.getBySha256sum(sha256sum);
-        String hostPort = dataBlock.getHostPort();
-        String[] arr = hostPort.split(":");
-        String host = arr[0];
-        int port = Integer.parseInt(arr[1]);
-        StoreNode storeNode = storeNodeMapper.findByNodeAddrAndHttpPort(host, port);
-        if (storeNode != null) {
-            StoreService storeService = rpcService.getStoreService(storeNode);
-            String absolutePath = dataBlock.getAbsolutePath();
-            String serverPartMd5 = storeService.getPartialMd5(absolutePath, offset, length);
-            result = serverPartMd5.equalsIgnoreCase(clientPartMd5);
-            if (result) {
-                long uploadBy = UserContext.getUserId();
-                int channelCode = uploadTask.getChannelCode();
-                UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(uploadBy, channelCode);
-                String channelPrefix = uploadChannel.getPrefix();
-                List<FileMeta> fileMetaList = fileMetaMapper.findBySha256sum(sha256sum);
-                if (!fileMetaList.isEmpty()) {
-                    FileMeta fileMeta0 = fileMetaList.get(0);
-                    String objectId = uploadTask.getUploadId();
-                    String filename = uploadTask.getFilename();
-                    String suffix = StringUtil.getSuffix(filename);
-                    String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
-                    int scope = uploadChannel.getScope();
-
-                    FileMeta fileMeta = new FileMeta(fileMeta0, objectId, objectName, filename, uploadBy, scope);
-                    fileMetaMapper.save(fileMeta);
-                } else {
-                    log.error("sha256sum {} not found any FileMeta", sha256sum);
-                }
-            }
-        }
-
-        return new UploadFileRet(uploadId, result);
-    }
-}

+ 46 - 52
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/OssClientService.java

@@ -2,11 +2,12 @@ package cn.reghao.oss.mgr.service;
 
 import cn.reghao.jutil.jdk.media.model.*;
 import cn.reghao.jutil.jdk.web.result.Result;
-import cn.reghao.jutil.jdk.web.result.WebResult;
 import cn.reghao.oss.api.constant.ObjectAction;
 import cn.reghao.oss.api.constant.ObjectScope;
-import cn.reghao.oss.api.constant.UploadStatus;
-import cn.reghao.oss.api.dto.*;
+import cn.reghao.oss.api.dto.ObjectChannel;
+import cn.reghao.oss.api.dto.ObjectInfo;
+import cn.reghao.oss.api.dto.ObjectMeta;
+import cn.reghao.oss.api.dto.ServerInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.iface.StoreService;
 import cn.reghao.oss.api.util.JwtUtils;
@@ -15,7 +16,6 @@ import cn.reghao.oss.mgr.config.UserContext;
 import cn.reghao.oss.mgr.db.mapper.DataBlockMapper;
 import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
 import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
-import cn.reghao.oss.mgr.db.mapper.UserKeyMapper;
 import cn.reghao.oss.mgr.db.repository.ObjectRepository;
 import cn.reghao.oss.mgr.model.po.*;
 import cn.reghao.oss.mgr.rpc.RpcService;
@@ -246,72 +246,65 @@ public class OssClientService {
     }
 
     public ObjectInfo getObjectInfo(String objectId) {
-        String host = "127.0.0.1";
-        int httpPort = 0;
-        String absolutePath = "";
-        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
-        if (dataBlock == null) {
+        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
+        if (fileMeta != null) {
+            String host = "";
+            int httpPort = 0;
+            String sha256sum = fileMeta.getSha256sum();
+            DataBlock dataBlock = dataBlockMapper.findBySha256sum(sha256sum);
             UploadTask uploadTask = uploadTaskMapper.findByUploadId(objectId);
-            if (uploadTask != null && uploadTask.getStatus() == UploadStatus.FLUSHING.getCode()) {
+            if (dataBlock != null) {
+                String hostPort = dataBlock.getHostPort();
+                String[] arr = hostPort.split(":");
+                host = arr[0];
+                httpPort = Integer.parseInt(arr[1]);
+            } else if (uploadTask != null) {
                 host = uploadTask.getHost();
                 httpPort = uploadTask.getHttpPort();
-            } else {
-                String errorMsg = String.format("object with id %s not exist", objectId);
-                throw new RuntimeException(errorMsg);
             }
-        } else {
-            String hostPort = dataBlock.getHostPort();
-            String[] arr = hostPort.split(":");
-            host = arr[0];
-            httpPort = Integer.parseInt(arr[1]);
-            absolutePath = dataBlock.getAbsolutePath();
-        }
 
-        StoreNode storeNode = storeNodeService.getStoreNode(host, httpPort);
-        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
-        String objectName = fileMeta.getObjectName();
-        int storeNodeId = storeNode.getId();
-        long uploadBy = fileMeta.getUploadBy();
-        UserNode userNode = userNodeService.getUserNode(uploadBy, storeNodeId);
-        String domain = userNode.getDomain();
-        String objectUrl = String.format("//%s/%s", domain, objectName);
+            if (!host.isEmpty() && httpPort != 0) {
+                String objectName = fileMeta.getObjectName();
+                StoreNode storeNode = storeNodeService.getStoreNode(host, httpPort);
+                int storeNodeId = storeNode.getId();
+                long uploadBy = fileMeta.getUploadBy();
+                UserNode userNode = userNodeService.getUserNode(uploadBy, storeNodeId);
+                String domain = userNode.getDomain();
+                String objectUrl = String.format("//%s/%s", domain, objectName);
+
+                ObjectInfo objectInfo = objectRepository.getObjectInfo(objectId);
+                objectInfo.setUrl(objectUrl);
+                fileMetaMapper.updateSetActiveByObjectId(objectId);
+                return objectInfo;
+            }
+        }
 
-        ObjectInfo objectInfo = objectRepository.getObjectInfo(objectId);
-        objectInfo.setUrl(objectUrl);
-        return objectInfo;
+        UploadTask uploadTask = uploadTaskMapper.findByUploadId(objectId);
+        String errorMsg = String.format("object with id %s not exist", objectId);
+        throw new RuntimeException(errorMsg);
     }
 
     public VideoInfo getVideoInfo(String objectId) {
-        String host = "127.0.0.1";
-        int httpPort = 0;
-        String absolutePath = "";
-        DataBlock dataBlock = dataBlockMapper.findByObjectId(objectId);
-        if (dataBlock == null) {
-            UploadTask uploadTask = uploadTaskMapper.findByUploadId(objectId);
-            if (uploadTask != null && uploadTask.getStatus() == UploadStatus.FLUSHING.getCode()) {
-                host = uploadTask.getHost();
-                httpPort = uploadTask.getHttpPort();
-            } else {
-                String errorMsg = String.format("object with id %s not exist", objectId);
-                throw new RuntimeException(errorMsg);
-            }
-        } else {
-            String hostPort = dataBlock.getHostPort();
-            String[] arr = hostPort.split(":");
-            host = arr[0];
-            httpPort = Integer.parseInt(arr[1]);
-            absolutePath = dataBlock.getAbsolutePath();
+        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
+        UploadTask uploadTask = uploadTaskMapper.findByUploadId(objectId);
+        if (fileMeta == null || uploadTask == null) {
+            String errorMsg = String.format("object with id %s not exist", objectId);
+            throw new RuntimeException(errorMsg);
         }
 
+        String host = uploadTask.getHost();
+        int httpPort = uploadTask.getHttpPort();
+        String absolutePath = "";
+        String sha256sum = fileMeta.getSha256sum();
+
         StoreNode storeNode = storeNodeService.getStoreNode(host, httpPort);
         StoreService storeService = rpcService.getStoreService(storeNode);
-        MediaProps mediaProps = storeService.getMediaInfo(objectId, absolutePath);
+        MediaProps mediaProps = storeService.getMediaInfo(objectId, sha256sum);
         if (mediaProps.getVideoProps() == null) {
             throw new RuntimeException(String.format("object with id %s not contain VideoProps", objectId));
         }
 
         VideoInfo videoInfo = new VideoInfo(objectId, mediaProps);
-        FileMeta fileMeta = fileMetaMapper.findByObjectId(objectId);
         String objectName = fileMeta.getObjectName();
         int storeNodeId = storeNode.getId();
         long uploadBy = fileMeta.getUploadBy();
@@ -324,6 +317,7 @@ public class OssClientService {
 
         MediaResolution mediaResolution = MediaQuality.getQuality(videoInfo.getWidth(), videoInfo.getHeight());
         videoInfo.setQuality(mediaResolution.getQualityStr());
+        fileMetaMapper.updateSetActiveByObjectId(objectId);
         return videoInfo;
     }
 }

+ 125 - 0
oss-mgr/src/main/java/cn/reghao/oss/mgr/service/PartUploadService.java

@@ -0,0 +1,125 @@
+package cn.reghao.oss.mgr.service;
+
+import cn.reghao.oss.api.constant.UploadStatus;
+import cn.reghao.oss.api.dto.rest.UploadFileRet;
+import cn.reghao.oss.api.dto.rest.UploadPrepare;
+import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
+import cn.reghao.oss.api.iface.StoreService;
+import cn.reghao.oss.mgr.config.UserContext;
+import cn.reghao.oss.mgr.db.mapper.FileMetaMapper;
+import cn.reghao.oss.mgr.db.mapper.StoreNodeMapper;
+import cn.reghao.oss.mgr.db.mapper.UploadChannelMapper;
+import cn.reghao.oss.mgr.db.mapper.UploadTaskMapper;
+import cn.reghao.oss.mgr.db.repository.ObjectRepository;
+import cn.reghao.oss.api.dto.rest.UploadSample;
+import cn.reghao.oss.mgr.model.po.*;
+import cn.reghao.oss.mgr.rpc.RpcService;
+import cn.reghao.oss.mgr.util.StringUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Random;
+import java.util.UUID;
+
+/**
+ * @author reghao
+ * @date 2026-02-01 15:49:10
+ */
+@Slf4j
+@Service
+public class PartUploadService {
+    private static final int CHUNK_SIZE = 1024 * 1024; // 采样块大小:1MB
+    private static final int SPLIT_SIZE = 1024 * 1024 * 10; // 分片大小:10MB
+    private final ObjectRepository objectRepository;
+    private final StoreNodeMapper storeNodeMapper;
+    private final UploadTaskMapper uploadTaskMapper;
+    private final RpcService rpcService;
+    private final FileMetaMapper fileMetaMapper;
+    private final UploadChannelMapper uploadChannelMapper;
+
+    public PartUploadService(ObjectRepository objectRepository, StoreNodeMapper storeNodeMapper,
+                             UploadTaskMapper uploadTaskMapper, RpcService rpcService, FileMetaMapper fileMetaMapper,
+                             UploadChannelMapper uploadChannelMapper) {
+        this.objectRepository = objectRepository;
+        this.storeNodeMapper = storeNodeMapper;
+        this.uploadTaskMapper = uploadTaskMapper;
+        this.rpcService = rpcService;
+        this.fileMetaMapper = fileMetaMapper;
+        this.uploadChannelMapper = uploadChannelMapper;
+    }
+
+    public UploadPrepareRet prepareUpload(UploadPrepare uploadPrepare) {
+        String uploadId = UUID.randomUUID().toString().replace("-", "");
+        long size = uploadPrepare.getSize();
+        String sha256sum = uploadPrepare.getSha256sum();
+        DataBlock dataBlock = objectRepository.getBySha256sum(sha256sum);
+        UploadPrepareRet uploadPrepareRet;
+        long randomOffset = 0;
+        if (dataBlock != null) {
+            if (size > CHUNK_SIZE) {
+                Random random = new Random(size);
+                randomOffset = (long) (random.nextDouble() * (size - CHUNK_SIZE));
+            }
+
+            boolean exist = true;
+            uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE, exist, randomOffset, CHUNK_SIZE);
+        } else {
+            long splitNumber = size/SPLIT_SIZE + (size%SPLIT_SIZE != 0 ? 1 : 0);
+            uploadPrepareRet = new UploadPrepareRet(uploadId, SPLIT_SIZE);
+        }
+
+        UploadTask uploadTask = new UploadTask(uploadId, uploadPrepare, randomOffset, CHUNK_SIZE, SPLIT_SIZE);
+        uploadTaskMapper.save(uploadTask);
+        return uploadPrepareRet;
+    }
+
+    public UploadFileRet verifyPartialHash(UploadSample uploadSample) {
+        String uploadId = uploadSample.getUploadId();
+        String clientPartMd5 = uploadSample.getSampleMd5();
+        UploadTask uploadTask = uploadTaskMapper.findByUploadId(uploadId);
+        boolean result = false;
+        if (uploadTask != null) {
+            long offset = uploadTask.getOffset();
+            int length = uploadTask.getLength();
+            String sha256sum = uploadTask.getSha256sum();
+            DataBlock dataBlock = objectRepository.getBySha256sum(sha256sum);
+            String hostPort = dataBlock.getHostPort();
+            String[] arr = hostPort.split(":");
+            String host = arr[0];
+            int port = Integer.parseInt(arr[1]);
+            StoreNode storeNode = storeNodeMapper.findByNodeAddrAndHttpPort(host, port);
+            if (storeNode != null) {
+                StoreService storeService = rpcService.getStoreService(storeNode);
+                String absolutePath = dataBlock.getAbsolutePath();
+                String serverPartMd5 = storeService.getPartialMd5(absolutePath, offset, length);
+                result = serverPartMd5.equalsIgnoreCase(clientPartMd5);
+                if (result) {
+                    long uploadBy = UserContext.getUserId();
+                    int channelCode = uploadTask.getChannelCode();
+                    UploadChannel uploadChannel = uploadChannelMapper.findByCreateByAndChannelCode(uploadBy, channelCode);
+                    String channelPrefix = uploadChannel.getPrefix();
+                    List<FileMeta> fileMetaList = fileMetaMapper.findBySha256sum(sha256sum);
+                    if (!fileMetaList.isEmpty()) {
+                        FileMeta fileMeta0 = fileMetaList.getFirst();
+                        String objectId = uploadTask.getUploadId();
+                        String filename = uploadTask.getFilename();
+                        String suffix = StringUtil.getSuffix(filename);
+                        String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
+                        int scope = uploadChannel.getScope();
+
+                        FileMeta fileMeta = new FileMeta(fileMeta0, objectId, objectName, filename, uploadBy, scope);
+                        uploadTask.setStatus(UploadStatus.AVAILABLE.getCode());
+                        uploadTask.setHost(host);
+                        uploadTask.setHttpPort(port);
+                        objectRepository.saveFastUpload(fileMeta, uploadTask);
+                    } else {
+                        log.error("sha256sum {} not found any FileMeta", sha256sum);
+                    }
+                }
+            }
+        }
+
+        return new UploadFileRet(uploadId, result);
+    }
+}

+ 9 - 4
oss-mgr/src/main/resources/mapper/FileMetaMapper.xml

@@ -4,16 +4,16 @@
 <mapper namespace="cn.reghao.oss.mgr.db.mapper.FileMetaMapper">
     <insert id="save" useGeneratedKeys="true" keyProperty="id">
         insert into file_meta
-        (`object_name`,`object_id`,`pid`,`filename`,`size`,`file_type`,`content_type`,`sha256sum`,`upload_by`,`scope`)
+        (`object_name`,`object_id`,`pid`,`filename`,`size`,`file_type`,`content_type`,`sha256sum`,`upload_by`,`scope`,`active`)
         values
-        (#{objectName},#{objectId},#{pid},#{filename},#{size},#{fileType},#{contentType},#{sha256sum},#{uploadBy},#{scope})
+        (#{objectName},#{objectId},#{pid},#{filename},#{size},#{fileType},#{contentType},#{sha256sum},#{uploadBy},#{scope},#{active})
     </insert>
     <insert id="saveAll" useGeneratedKeys="true" keyProperty="id">
         insert into file_meta
-        (`object_name`,`object_id`,`pid`,`filename`,`size`,`file_type`,`content_type`,`sha256sum`,`upload_by`,`scope`)
+        (`object_name`,`object_id`,`pid`,`filename`,`size`,`file_type`,`content_type`,`sha256sum`,`upload_by`,`scope`,`active`)
         values
         <foreach collection="list" item="item" index="index" separator=",">
-            (#{item.objectName},#{item.objectId},#{item.pid},#{item.filename},#{item.size},#{item.fileType},#{item.contentType},#{item.sha256sum},#{item.uploadBy},#{item.scope})
+            (#{item.objectName},#{item.objectId},#{item.pid},#{item.filename},#{item.size},#{item.fileType},#{item.contentType},#{item.sha256sum},#{item.uploadBy},#{item.scope},#{item.active})
         </foreach>
     </insert>
 
@@ -37,6 +37,11 @@
         set update_time=now(),scope=#{scope}
         where object_id=#{objectId}
     </update>
+    <update id="updateSetActiveByObjectId">
+        update file_meta
+        set update_time=now(),active=1
+        where object_id=#{objectId}
+    </update>
 
     <select id="findAll" resultType="cn.reghao.oss.mgr.model.po.FileMeta">
         select *

+ 97 - 15
oss-sdk/src/main/java/cn/reghao/oss/sdk/OssClient.java

@@ -3,18 +3,19 @@ package cn.reghao.oss.sdk;
 import cn.reghao.jutil.jdk.io.FileSplitter;
 import cn.reghao.jutil.jdk.serializer.JsonConverter;
 import cn.reghao.jutil.jdk.web.result.WebResult;
-import cn.reghao.oss.api.dto.*;
+import cn.reghao.oss.api.dto.ObjectChannel;
+import cn.reghao.oss.api.dto.ObjectInfo;
+import cn.reghao.oss.api.dto.ServerInfo;
 import cn.reghao.oss.api.dto.media.VideoInfo;
-import cn.reghao.oss.api.dto.rest.UploadFileRet;
-import cn.reghao.oss.api.dto.rest.UploadPrepare;
-import cn.reghao.oss.api.dto.rest.UploadPrepareRet;
-import cn.reghao.oss.api.dto.rest.UploadSample;
+import cn.reghao.oss.api.dto.rest.*;
 import cn.reghao.oss.api.util.OssClientSigner;
 import cn.reghao.oss.api.util.OssSamplingHash;
 import com.google.gson.reflect.TypeToken;
 
 import java.io.ByteArrayInputStream;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.lang.reflect.Type;
 import java.net.URI;
 import java.net.http.HttpClient;
@@ -45,12 +46,11 @@ public class OssClient {
         this.sk = sk;
     }
 
-    public UploadPrepareRet prepareUpload(int channelCode, Path filePath) throws Exception {
+    public UploadPrepareRet prepareUpload(String clientSha256sum, int channelCode, Path filePath) throws Exception {
         // 1. 计算 SHA-256 (用于后端秒传校验)
-        String sha256 = OssSamplingHash.calculateFullHash(filePath);
         String filename0 = filePath.getFileName().toString();
         long size = filePath.toFile().length();
-        UploadPrepare uploadPrepare = new UploadPrepare(channelCode, filename0, size, sha256);
+        UploadPrepare uploadPrepare = new UploadPrepare(channelCode, filename0, size, clientSha256sum);
         return prepareUpload(uploadPrepare);
     }
 
@@ -186,7 +186,6 @@ public class OssClient {
                 .header("Content-Type", contentType)
                 .header("X-File-SHA256", sha256)
                 .header("X-File-Name", filename)
-                .header("X-Channel-Code", channelCode+"")
                 // JDK 17 的 BodyPublishers.ofFile 是流式的,不会爆内存
                 .POST(HttpRequest.BodyPublishers.ofFile(filePath))
                 .build();
@@ -200,7 +199,8 @@ public class OssClient {
     }
 
     public UploadFileRet uploadFilePart(int channelCode, String uploadUrl, String token, Path filePath) throws Exception {
-        UploadPrepareRet uploadPrepareRet = prepareUpload(channelCode, filePath);
+        String clientSha256sum = OssSamplingHash.calculateFullHash(filePath);
+        UploadPrepareRet uploadPrepareRet = prepareUpload(clientSha256sum, channelCode, filePath);
         if (uploadPrepareRet == null) {
             return null;
         }
@@ -236,7 +236,7 @@ public class OssClient {
             map.put(chunkNumber, start);
 
             int currentChunkSize = part.length;
-            UploadFilePart uploadFilePart = new UploadFilePart(101, identifier, filename, relativePath,
+            UploadFilePart uploadFilePart = new UploadFilePart(clientSha256sum, uploadId, identifier, filename, relativePath,
                     totalSize, chunkSize, totalChunks, chunkNumber, currentChunkSize);
 
             UploadFileRet uploadFileRet = postObject(uploadUrl, token, part, uploadFilePart);
@@ -276,7 +276,8 @@ public class OssClient {
     private UploadFileRet postObject(String uploadUrl, String uploadToken, byte[] bytes, UploadFilePart uploadFilePart) throws Exception {
         MultiPartBodyPublisher publisher = new MultiPartBodyPublisher();
         publisher
-                .addPart("channelCode", uploadFilePart.getChannelCode()+"")
+                .addPart("clientSha256sum", uploadFilePart.getClientSha256sum())
+                .addPart("uploadId", uploadFilePart.getUploadId())
                 .addPart("identifier", uploadFilePart.getIdentifier())
                 .addPart("filename", uploadFilePart.getFilename())
                 .addPart("relativePath", uploadFilePart.getRelativePath())
@@ -376,17 +377,98 @@ public class OssClient {
         return response.body();
     }
 
-    public boolean headObject(String sha256sum) {
+    public boolean headObject(int channelCode, File file) throws Exception {
+        ServerInfo serverInfo = getServerInfo(channelCode);
+        String uploadUrl = serverInfo.getOssUrl();
+        String uploadToken = serverInfo.getToken();
+
+        String clientSha256sum = OssSamplingHash.calculateFullHash(file.toPath());
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(uploadUrl))
+                .timeout(Duration.ofMinutes(5))
+                .HEAD()
+                .header("Authorization", "Bearer " + uploadToken)
+                .header("X-Sha256-Sum", clientSha256sum)
+                .build();
         return false;
     }
 
-    public UploadFileRet postObject(int channelCode, File file) throws Exception {
-        ServerInfo serverInfo = getServerInfo(channelCode);
+    public UploadFileRet postObject(ServerInfo serverInfo, File file) throws Exception {
         String uploadUrl = serverInfo.getOssUrl();
         String uploadToken = serverInfo.getToken();
+        String clientSha256sum = OssSamplingHash.calculateFullHash(file.toPath());
+
+        MultiPartBodyPublisher publisher = new MultiPartBodyPublisher();
+        publisher
+                .addPart("clientSha256sum", clientSha256sum)
+                .addPart("filename", file.getName())
+                .addPart("file", () -> {
+                    try {
+                        // 在真正发送数据、调用 get() 时才执行打开流
+                        return new FileInputStream(file);
+                    } catch (FileNotFoundException e) {
+                        // 【核心】转为运行时异常,终止 HttpClient 后续的错误发送
+                        throw new RuntimeException("准备上传时本地源文件突然丢失: " + file.getAbsolutePath(), e);
+                    }
+                }, file.getName(), "");
+
+        String api = String.format("%s/?multipart", uploadUrl);
+        HttpRequest httpRequest = HttpRequest.newBuilder(new URI(api))
+                .version(HttpClient.Version.HTTP_1_1)
+                .header("Authorization", "Bearer " + uploadToken)
+                .header("Content-Type", "multipart/form-data; boundary=" + publisher.getBoundary())
+                .POST(publisher.build())
+                .build();
+
+        HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());
+        int statusCode = httpResponse.statusCode();
+        if (statusCode == 200) {
+            String body = httpResponse.body();
+            Type type = new TypeToken<WebResult<UploadFileRet>>(){}.getType();
+            WebResult<UploadFileRet> webResult = JsonConverter.jsonToObject(body, type);
+            if (webResult.getCode() == 0 ) {
+                return webResult.getData();
+            }
+        }
         return null;
     }
 
+    public UploadFileRet putObject(ServerInfo serverInfo, File file) throws Exception {
+        String uploadUrl = serverInfo.getOssUrl();
+        String uploadToken = serverInfo.getToken();
+        String clientSha256sum = OssSamplingHash.calculateFullHash(file.toPath());
+        String contentType = "";
+        Path path = Paths.get(file.getPath());
+        // 1. 构建 PUT 请求
+        HttpRequest request = HttpRequest.newBuilder()
+                .uri(URI.create(uploadUrl))
+                .timeout(Duration.ofMinutes(5)) // 根据大文件调整超时时间
+                // 核心:直接传入 Path,HttpClient 会在底层执行零内存占用的流式读取
+                .PUT(HttpRequest.BodyPublishers.ofFile(path))
+                .header("Authorization", "Bearer " + uploadToken)
+                .header("Content-Type", contentType)
+                .header("X-File-SHA256", clientSha256sum)
+                .header("X-File-Name", file.getName())
+                .build();
+
+        // 2. 发送请求并接收响应
+        // HttpResponse.BodyHandlers.ofString() 会将服务器返回的 Body 转为字符串
+        HttpResponse<String> httpResponse = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
+        int statusCode = httpResponse.statusCode();
+        String errorMsg = "";
+        if (statusCode == 200) {
+            String body = httpResponse.body();
+            Type type = new TypeToken<WebResult<UploadFileRet>>(){}.getType();
+            WebResult<UploadFileRet> webResult = JsonConverter.jsonToObject(body, type);
+            if (webResult.getCode() == 0 ) {
+                return webResult.getData();
+            }
+
+            errorMsg = webResult.getMsg();
+        }
+        throw new RuntimeException(String.format("statusCode: %s, errorMsg: %s", statusCode, errorMsg));
+    }
+
     public ObjectChannel getChannel(int channelCode) {
         String api = "/api/oss/sdk/channel";
         String queryString = "channelCode=" + channelCode;

+ 18 - 15
oss-store/src/main/java/cn/reghao/oss/store/disk/HddFlushService.java

@@ -1,7 +1,7 @@
 package cn.reghao.oss.store.disk;
 
 import cn.reghao.jutil.jdk.thread.ThreadFactoryBuilder;
-import cn.reghao.oss.api.dto.UploadDoneResult;
+import cn.reghao.oss.api.dto.rest.UploadDoneResult;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.store.config.OssStoreConfig;
 import lombok.extern.slf4j.Slf4j;
@@ -59,22 +59,27 @@ public class HddFlushService {
      * @return
      * @date 2026-04-28 11:48:56
      */
-    public void triggerFlush(String uploadId) {
+    public void triggerFlush(String uploadId, String clientSha256sum) {
         moveExecutor.submit(() -> {
             try {
                 String ssdPath = diskService.getSsdTempPath(uploadId);
-                String hddPath = diskService.getHddTempPath(uploadId);
+                String hddTempPath = diskService.getHddTempPath(uploadId);
 
                 File ssdFile = new File(ssdPath);
                 long size = ssdFile.length();
                 // 1. 搬运前的空间预检
-                checkDiskSpace(size, hddPath);
+                checkDiskSpace(size, hddTempPath);
 
-                FlushResult flushResult = moveAndChecksum(ssdPath, hddPath);
+                // 将 ssd 中的文件移动到 hdd
+                FlushResult flushResult = moveAndChecksum(ssdPath, hddTempPath);
                 String sha256sum = flushResult.sha256sum;
-                String contentType = flushResult.contentType;
+                if (!sha256sum.equals(clientSha256sum)) {
+                    log.error("sha256sum not match clientSha256sum, upload {} failed", uploadId);
+                    return;
+                }
 
-                Path tmpFile = Path.of(hddPath);
+                String contentType = flushResult.contentType;
+                Path tmpFile = Path.of(hddTempPath);
                 String filePath = diskService.getHddDataPath(sha256sum);
                 Path finalPath = Path.of(filePath);
                 // hdd 磁盘内部的原子移动:这是文件系统层面的操作,极快
@@ -83,11 +88,6 @@ public class HddFlushService {
 
                 UploadDoneResult uploadDoneResult = new UploadDoneResult(uploadId, sha256sum, absolutePath, size, hostPort);
                 consoleService.updateAfterMove(uploadDoneResult);
-                log.info("{} 的 HDD 搬运完成", uploadId);
-
-                // 清理 SSD 临时文件
-                log.info("清理 SSD 临时文件 {}", ssdPath);
-                //Files.deleteIfExists(Paths.get(ssdPath));
             } catch (InsufficientSpaceException e) {
                 log.error("HDD 空间不足,取消搬运: {}", e.getMessage());
             } catch (Exception e) {
@@ -110,12 +110,15 @@ public class HddFlushService {
                 Path targetPath = Path.of(destPath);
 
                 checkDiskSpace(size, destPath);
-                // 移动文件 (SSD -> HDD)
-                Files.move(Paths.get(tempPath), targetPath, StandardCopyOption.REPLACE_EXISTING);
+                Path srcPath = Paths.get(tempPath);
+                // ssd 到 hdd 的跨磁盘拷贝, 此时 SSD 文件完好,getVideoInfo 随时可以并发读取它
+                Files.copy(srcPath, targetPath, StandardCopyOption.REPLACE_EXISTING);
                 UploadDoneResult uploadDoneResult = new UploadDoneResult(uploadId, sha256sum, destPath, size, hostPort);
                 try {
-                    // 3. 尝试通知 oss-mgr
+                    // 尝试通知 oss-mgr
                     consoleService.updateAfterMove(uploadDoneResult);
+                    // 最后,安全地删除 SSD 临时文件
+                    Files.deleteIfExists(srcPath);
                 } catch (Exception e) {
                     log.error("{}", e.getMessage());
                     // 4. 通知失败,转入本地补偿

+ 1 - 1
oss-store/src/main/java/cn/reghao/oss/store/disk/MetadataCompensator.java

@@ -1,6 +1,6 @@
 package cn.reghao.oss.store.disk;
 
-import cn.reghao.oss.api.dto.UploadDoneResult;
+import cn.reghao.oss.api.dto.rest.UploadDoneResult;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.store.config.OssStoreConfig;
 import com.fasterxml.jackson.databind.ObjectMapper;

+ 14 - 31
oss-store/src/main/java/cn/reghao/oss/store/handler/OssMultipartUploadHandler.java

@@ -1,10 +1,9 @@
 package cn.reghao.oss.store.handler;
 
-import cn.reghao.jutil.jdk.security.DigestUtil;
 import cn.reghao.jutil.jdk.web.result.WebResult;
 import cn.reghao.oss.api.constant.UploadStatus;
-import cn.reghao.oss.api.dto.FastUploadResult;
-import cn.reghao.oss.api.dto.UploadResult;
+import cn.reghao.oss.api.dto.rest.FastUploadResult;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 import cn.reghao.oss.api.dto.rest.UploadFileRet;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.api.util.FileUtil;
@@ -14,7 +13,6 @@ import cn.reghao.oss.store.disk.DiskService;
 import cn.reghao.oss.store.disk.HddFlushService;
 import cn.reghao.oss.store.util.ResponseHelper;
 import cn.reghao.oss.store.util.UploadProgressManager;
-import cn.reghao.oss.store.util.UploadState;
 import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.ChannelHandlerContext;
@@ -25,8 +23,6 @@ import io.netty.util.AttributeKey;
 import io.netty.util.ReferenceCountUtil;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.tika.Tika;
-import org.apache.tika.metadata.Metadata;
-import org.apache.tika.metadata.TikaCoreProperties;
 
 import java.io.File;
 import java.io.IOException;
@@ -54,7 +50,6 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
     // 当前请求的上下文状态
     private static final Tika tika = new Tika();
     private final Map<String, String> formData = new HashMap<>();
-    private final OssStoreConfig ossStoreConfig;
     private final ConsoleService consoleService;
     private final HddFlushService hddFlushService;
     private final DiskService diskService;
@@ -64,11 +59,10 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
     // 8 字节
     private final ByteBuf headerCollector = Unpooled.buffer(8);
     private boolean isTypeIdentified = false;
-    private final String hostPort;
+    private final OssStoreConfig ossStoreConfig;
 
     public OssMultipartUploadHandler(OssStoreConfig ossStoreConfig, ConsoleService consoleService,
                                      HddFlushService hddFlushService, DiskService diskService) {
-        this.hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
         this.ossStoreConfig = ossStoreConfig;
         this.consoleService = consoleService;
         this.hddFlushService = hddFlushService;
@@ -130,16 +124,11 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
         String name = attr.getName();
         String value = attr.getValue();
         formData.put(name, value);
-        /*if ("identifier".equals(name)) currentUploadId = value;
-        if ("chunkNumber".equals(name)) currentChunkNumber = Integer.parseInt(value);
-        if ("chunkSize".equals(name)) chunkSize = Long.parseLong(value);
-        if ("totalChunks".equals(name)) totalParts = Integer.parseInt(value);
-        if ("filename".equals(name)) filename = value;*/
     }
 
     // 处理 multipart/form-data 的文件
     private void processFileUpload(FileUpload fileUpload) throws IOException {
-        String currentUploadId = formData.get("identifier");
+        String currentUploadId = formData.get("uploadId");
         if (currentUploadId == null) {
             handleSingleFileUploadStreamingly(fileUpload);
             return;
@@ -163,8 +152,6 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
         // 1. 初始化文件 ID 和 Channel (仅在第一次进入时)
         if (singleFileId == null) {
             singleFileId = UUID.randomUUID().toString().replace("-", "");
-            formData.put("uploadId", singleFileId);
-
             String filename = fileUpload.getFilename();
             formData.put("filename", filename);
 
@@ -228,7 +215,6 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
             byte[] hashBytes = messageDigest.digest();
             String sha256sum = OssSamplingHash.bytesToHex(hashBytes);
             formData.put("sha256sum", sha256sum);
-            log.info("文件上传完成,SHA-256: {}", sha256sum);
         }
     }
 
@@ -261,7 +247,6 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
 
             // 写入成功后标记位图
             UploadProgressManager.markPart(uploadId, currentChunkNumber);
-            System.out.printf("分片写入成功: ID=%s, Offset=%s, Size=%s\n", uploadId, offset, fileUpload.length());
         } catch (Exception e) {
             log.error("{}", e.getMessage());
         }
@@ -270,12 +255,13 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
     private void finalizeUpload(ChannelHandlerContext ctx) throws Exception {
         String channelPrefix = (String) ctx.channel().attr(AttributeKey.valueOf("channelPrefix")).get();
         long uploadBy = (Long) ctx.channel().attr(AttributeKey.valueOf("uploadBy")).get();
-        String currentUploadId = formData.get("identifier");
+        String currentUploadId = formData.get("uploadId");
         if (currentUploadId == null) {
             // 单文件上传
-            String uploadId = formData.get("uploadId");
+            String uploadId = singleFileId;
             String filename = formData.get("filename");
             String contentType = formData.get("sniffedContentType");
+            String clientSha256sum = formData.get("clientSha256sum");
             String sha256sum = formData.get("sha256sum");
 
             String ssdTempPath = diskService.getSsdTempPath(uploadId);
@@ -292,12 +278,12 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
             uploadResult.setObjectId(uploadId);
             uploadResult.setObjectName(objectName);
             uploadResult.setUploadBy(uploadBy);
-            uploadResult.setHostPort(hostPort);
+            uploadResult.setHost(ossStoreConfig.getStoreHost());
+            uploadResult.setPort(ossStoreConfig.getPort());
             uploadResult.setUploadStatus(UploadStatus.FLUSHING.getCode());
 
             FastUploadResult fastUploadResult = new FastUploadResult(uploadResult);
             if (consoleService.checkExists(uploadResult.getSha256sum())) {
-                log.info("文件 {} 触发秒传, 删除 ssd 中的临时文件 {}", uploadResult.getSha256sum(), ssdTempPath);
                 consoleService.bindOnly(fastUploadResult);
             } else {
                 consoleService.registerAndBind(uploadResult);
@@ -315,7 +301,6 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
         // 1. 业务逻辑处理 (Bitmap 等)
         int totalParts = Integer.parseInt(formData.get("totalChunks"));
         if (UploadProgressManager.isComplete(currentUploadId, totalParts)) {
-            log.info("文件 {} 校验通过,准备触发 HDD 异步搬运", currentUploadId);
             String uploadId = currentUploadId;
             String filename = formData.get("filename");
             String ssdTempPath = diskService.getSsdTempPath(uploadId);
@@ -323,9 +308,10 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
             long size = tmpFile.length();
             String contentType = getContentType(tmpFile);
             String objectName = getObjectName(uploadId, filename, contentType, channelPrefix);
+            String clientSha256sum = formData.get("clientSha256sum");
 
             UploadResult uploadResult = new UploadResult();
-            //uploadResult.setSha256sum("");
+            uploadResult.setSha256sum(clientSha256sum);
             //uploadResult.setAbsolutePath("");
             uploadResult.setSize(size);
             uploadResult.setFilename(filename);
@@ -334,12 +320,13 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
             uploadResult.setObjectId(uploadId);
             uploadResult.setObjectName(objectName);
             uploadResult.setUploadBy(uploadBy);
-            uploadResult.setHostPort(hostPort);
+            uploadResult.setHost(ossStoreConfig.getStoreHost());
+            uploadResult.setPort(ossStoreConfig.getPort());
             uploadResult.setUploadStatus(UploadStatus.FLUSHING.getCode());
             consoleService.registerAndBind(uploadResult);
 
             // 2. 触发合并逻辑 (合并 SSD 上的碎片)
-            hddFlushService.triggerFlush(currentUploadId);
+            hddFlushService.triggerFlush(currentUploadId, clientSha256sum);
             // 3. 清理内存位图
             UploadProgressManager.remove(currentUploadId);
 
@@ -348,14 +335,10 @@ public class OssMultipartUploadHandler extends SimpleChannelInboundHandler<HttpO
             UploadFileRet uploadFileRet = new UploadFileRet(currentUploadId, url, true);
             String jsonResult = WebResult.success(uploadFileRet);
             ResponseHelper.sendJsonResponse(ctx, jsonResult);
-            log.info("{} 的分片上传已完成并已回写响应", currentUploadId);
         } else {
             UploadFileRet uploadFileRet = new UploadFileRet(currentUploadId);
             String jsonResult = WebResult.success(uploadFileRet);
             ResponseHelper.sendJsonResponse(ctx, jsonResult);
-
-            int currentChunkNumber = Integer.parseInt(formData.get("chunkNumber"));
-            log.info("分片 {} 写入完成并已回写响应", currentChunkNumber);
         }
     }
 

+ 28 - 34
oss-store/src/main/java/cn/reghao/oss/store/handler/OssRouterHandler.java

@@ -50,7 +50,7 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
             if (HttpMethod.OPTIONS.equals(method)) {
                 handleOptions(ctx, req);
                 return; // 拦截并结束,不传递给后续 Handler
-            } else if (HttpMethod.POST.equals(method)) {
+            } else if (HttpMethod.PUT.equals(method) || HttpMethod.POST.equals(method)) {
                 String headerValue = headers.get("authorization");
                 if (headerValue != null && headerValue.startsWith("Bearer")) {
                     String token = headerValue.replace("Bearer ", "");
@@ -61,26 +61,27 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
                         return;
                     }
 
-                    String action = String.valueOf(claims.get("action"));
+                    Object actionObj = claims.get("action");
+                    Object uploadByObj = claims.get("uploadBy");
+                    Object maxSizeObj = claims.get("maxSize");
+                    Object channelPrefixObj = claims.get("channelPrefix");
+                    Object contentTypeObj = claims.get("contentType");
+
                     long uploadBy = -1;
-                    if (claims.get("uploadBy") instanceof Integer) {
-                        int uploadByInt = (int) claims.get("uploadBy");
-                        uploadBy = uploadByInt;
-                    } else if (claims.get("uploadBy") instanceof Long) {
-                        uploadBy = (long) claims.get("uploadBy");
+                    if (uploadByObj instanceof Long) {
+                        uploadBy = (long) uploadByObj;
+                    } else if (uploadByObj instanceof Integer) {
+                        uploadBy = (int) uploadByObj;
                     }
-
                     long maxSize = 0L;
-                    if (claims.get("maxSize") instanceof Integer) {
-                        int maxSizeInt = (int) claims.get("maxSize");
-                        maxSize = maxSizeInt;
-                    } else if (claims.get("maxSize") instanceof Long) {
-                        maxSize = (long) claims.get("maxSize");;
+                    if (maxSizeObj instanceof Long) {
+                        maxSize = (long) maxSizeObj;
+                    } else if (maxSizeObj instanceof Integer) {
+                        maxSize = (int) maxSizeObj;
                     }
-
                     // TODO 验证上传文件的 contentType 是否符合 channelPrefix 的配置, 若不符合则立即终止上传
-                    String channelPrefix = (String) claims.get("channelPrefix");
-                    String contentType = (String) claims.get("contentType");
+                    String channelPrefix = (String) channelPrefixObj;
+                    String contentType = (String) contentTypeObj;
 
                     // 将上传信息注入后续的业务 Handler
                     ctx.channel().attr(AttributeKey.valueOf("uploadBy")).set(uploadBy);
@@ -93,25 +94,18 @@ public class OssRouterHandler extends SimpleChannelInboundHandler<HttpObject> {
                 }
 
                 String contentType = headers.get("content-type");
-                List<String> valueList = decoder.parameters().get("multipart");
-                if (valueList == null) {
-                    valueList = new ArrayList<>();
-                }
-
-                if (contentType.contains("multipart/form-data")) {
-                    // 浏览器模式:切换到 OssMultipartUploadHandler
-                    switchTo(ctx, "browser-uploader",
-                            new OssMultipartUploadHandler(ossStoreConfig, consoleService, hddFlushService, diskService));
-                } else if (!valueList.isEmpty()) {
-                    // 分片上传:路径匹配且无分片参数
-                    switchTo(ctx, "multipart",
-                            new OssMultipartUploadHandler(ossStoreConfig, consoleService, hddFlushService, diskService));
-                } else if (path.startsWith("/")) {
-                    // 普通单上传:路径匹配且无分片参数
-                    switchTo(ctx,"single-file",  new OssUploadHandler(ossStoreConfig, consoleService, hddFlushService));
+                if (HttpMethod.PUT.equals(method)) {
+                    // PUT 单文件上传:直接切到单文件流式处理器
+                    switchTo(ctx, "single-put-file", new OssUploadHandler(ossStoreConfig, consoleService, hddFlushService));
                 } else {
-                    sendError(ctx, HttpResponseStatus.BAD_REQUEST);
-                    return;
+                    if (contentType != null && contentType.contains("multipart/form-data")) {
+                        // 浏览器模式:切换到 OssMultipartUploadHandler
+                        switchTo(ctx, "browser-uploader",
+                                new OssMultipartUploadHandler(ossStoreConfig, consoleService, hddFlushService, diskService));
+                    } else {
+                        // 普通单上传, 也就是接收裸流
+                        switchTo(ctx,"single-file",  new OssUploadHandler(ossStoreConfig, consoleService, hddFlushService));
+                    }
                 }
             } else if (HttpMethod.GET.equals(method)) {
                 // 下载逻辑

+ 15 - 8
oss-store/src/main/java/cn/reghao/oss/store/handler/OssUploadHandler.java

@@ -1,8 +1,8 @@
 package cn.reghao.oss.store.handler;
 
 import cn.reghao.jutil.jdk.web.result.WebResult;
-import cn.reghao.oss.api.dto.FastUploadResult;
-import cn.reghao.oss.api.dto.UploadResult;
+import cn.reghao.oss.api.dto.rest.FastUploadResult;
+import cn.reghao.oss.api.dto.rest.UploadResult;
 import cn.reghao.oss.api.dto.rest.UploadFileRet;
 import cn.reghao.oss.api.iface.ConsoleService;
 import cn.reghao.oss.api.util.FileUtil;
@@ -19,6 +19,8 @@ import io.netty.util.ReferenceCounted;
 import lombok.extern.slf4j.Slf4j;
 
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.UUID;
 
 /**
@@ -90,18 +92,20 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
                 FastUploadResult fastUploadResult = new FastUploadResult(clientSha256, filename, objectId, objectName, uploadBy);
                 consoleService.bindOnly(fastUploadResult);
                 String uploadId = fastUploadResult.getObjectId();
-                // 返回成功响应
+
+                // 返回响应
                 String url = "";
                 UploadFileRet uploadFileRet = new UploadFileRet(uploadId, url, true);
                 String jsonResult = WebResult.success(uploadFileRet);
                 ResponseHelper.sendJsonResponse(ctx, jsonResult);
+
                 ctx.channel().attr(SKIP_BODY).set(true);
                 return;
             }
 
             String tempFilePath = String.format("%s/%s.tmp", ssdTemp, objectId);
-            UploadState state = new UploadState(objectId, tempFilePath, filename);
-            ctx.channel().attr(STATE_KEY).set(state);
+            UploadState uploadState = new UploadState(objectId, tempFilePath, filename);
+            ctx.channel().attr(STATE_KEY).set(uploadState);
         } else if (msg instanceof HttpContent) {
             // 2. 处理上传 Body
             handleUploadContent(ctx, (HttpContent) msg);
@@ -151,8 +155,6 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
 
             String suffix = FileUtil.getSuffix(filename);
             String objectName = String.format("%s%s.%s", channelPrefix, uploadId, suffix);
-
-            String hostPort = String.format("%s:%s", ossStoreConfig.getStoreHost(), ossStoreConfig.getPort());
             UploadResult uploadResult = new UploadResult();
             uploadResult.setSha256sum(sha256sum);
             //uploadResult.setAbsolutePath("");
@@ -163,11 +165,13 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
             uploadResult.setObjectId(uploadId);
             uploadResult.setObjectName(objectName);
             uploadResult.setUploadBy(uploadBy);
-            uploadResult.setHostPort(hostPort);
+            uploadResult.setHost(ossStoreConfig.getStoreHost());
+            uploadResult.setPort(ossStoreConfig.getPort());
 
             consoleService.registerAndBind(uploadResult);
             hddFlushService.moveSsdToHdd(uploadId, sha256sum, size, ssdTempPath);
 
+            // 返回响应
             String url = "";
             UploadFileRet uploadFileRet = new UploadFileRet(uploadId, url);
             String jsonResult = WebResult.success(uploadFileRet);
@@ -175,6 +179,9 @@ public class OssUploadHandler extends SimpleChannelInboundHandler<HttpObject> {
         }
     }
 
+    /**
+     * 清理未能成功上传完的残余垃圾临时文件
+     */
     private void cleanUpPartialFile(ChannelHandlerContext ctx) {
     }
 

+ 123 - 2
oss-store/src/main/java/cn/reghao/oss/store/rpc/StoreServiceImpl.java

@@ -4,6 +4,7 @@ import cn.reghao.jutil.jdk.media.FFmpegWrapper;
 import cn.reghao.jutil.jdk.media.model.MediaProps;
 import cn.reghao.oss.api.dto.disk.DiskVolume;
 import cn.reghao.oss.api.dto.media.ImageInfo;
+import cn.reghao.oss.api.dto.media.VideoInfo;
 import cn.reghao.oss.api.iface.StoreService;
 import cn.reghao.oss.api.util.OssSamplingHash;
 import cn.reghao.oss.store.disk.DiskService;
@@ -11,9 +12,12 @@ import lombok.extern.slf4j.Slf4j;
 import org.apache.dubbo.config.annotation.DubboService;
 import org.springframework.stereotype.Service;
 
-import java.io.File;
+import java.io.*;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.stream.Collectors;
 
 /**
  * 获取存储节点信息
@@ -26,6 +30,7 @@ import java.util.List;
 @DubboService
 @Service
 public class StoreServiceImpl implements StoreService {
+    private final static String ffprobe = "/usr/bin/ffprobe";
     private final DiskService diskService;
 
     public StoreServiceImpl(DiskService diskService) {
@@ -52,8 +57,13 @@ public class StoreServiceImpl implements StoreService {
         }
     }
 
-    public MediaProps getMediaInfo(String objectId, String absolutePath) {
+    public MediaProps getMediaInfo0(String objectId, String absolutePath) {
+        String sha256sum = "";
         try {
+            String ssdTempPath = diskService.getSsdTempPath(objectId);
+            String hddTempPath = diskService.getHddTempPath(objectId);
+            String hddDataPath = diskService.getHddDataPath(sha256sum);
+
             File file = new File(absolutePath);
             if (!file.exists()) {
                 String tmpPath = diskService.getSsdTempPath(objectId);
@@ -69,6 +79,117 @@ public class StoreServiceImpl implements StoreService {
         }
     }
 
+    public MediaProps getMediaInfo(String objectId, String sha256sum) {
+        try {
+            String ssdTempPath = diskService.getSsdTempPath(objectId);
+            String hddTempPath = diskService.getHddTempPath(objectId);
+            String hddDataPath = diskService.getHddDataPath(sha256sum);
+
+            // 1. 建立探测优先级队列 (基于最可能存在的状态预测)
+            File hddDataFile = new File(hddDataPath);
+            File hddTempFile = new File(hddTempPath);
+            File ssdTempFile = new File(ssdTempPath);
+
+            File primaryFile;
+            if (hddDataFile.exists()) {
+                primaryFile = hddDataFile; // 大概率已经归档成功
+            } else if (hddTempFile.exists()) {
+                primaryFile = hddTempFile; // 正在 HDD 临时区
+            } else {
+                primaryFile = ssdTempFile; // 刚上传完,还在 SSD
+            }
+
+            // 2. 尝试原子性打开句柄
+            try {
+                return openAndExtract(primaryFile);
+            } catch (FileNotFoundException e) {
+                log.warn("[IO 临界点触发] 预测路径 {} 失效,启动全路径轮询兜底. objectId: {}", primaryFile.getName(), objectId);
+
+                // 3. 兜底防线:如果刚才预测的路径在打开的一瞬间被 Files.move 删了,说明它正在发生位移
+                // 此时我们不计成本,直接按逆向顺序(最终->临时)强行捞一遍
+
+                // 再次尝试最终归档路径
+                try { return openAndExtract(hddDataFile); } catch (FileNotFoundException ignored) {}
+
+                // 再次尝试 HDD 临时路径
+                try { return openAndExtract(hddTempFile); } catch (FileNotFoundException ignored) {}
+
+                // 再次尝试 SSD 临时路径
+                try { return openAndExtract(ssdTempFile); } catch (FileNotFoundException ignored) {}
+
+                // 4. 三级防御全部击穿,说明文件真的不存在或损坏
+                log.error("[严重错误] 文件在三层存储结构中彻底消失! objectId: {}, sha256: {}", objectId, sha256sum);
+                throw new RuntimeException("File not found in any storage tiers");
+            }
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private MediaProps openAndExtract(File file) throws FileNotFoundException {
+        // 1. Java 瞬间打开并锁定 Inode,无视后续的删除操作
+        try (FileInputStream fis = new FileInputStream(file)) {
+            List<String> cmd = Arrays.asList(
+                    ffprobe,
+                    "-v", "quiet",           // 不打印日志头
+                    "-print_format", "json", // 输出 JSON
+                    "-show_format",          // 容器格式信息 (bitrate, size, duration, tags)
+                    "-show_streams",         // 所有流 (video, audio, subtitle, data)
+                    "-show_chapters",        // 章节信息
+                    "-show_programs",        // 节目信息 (常用于 TS 流)
+                    "-show_error",           // 如果解析出错,输出错误 JSON
+                    "-"                     // 【核心】从 stdin 读取
+            );
+
+            // 2. 构建 FFmpeg / FFprobe 进程
+            // 使用 "-" 代表从标准输入(stdin)读取数据,-max_analyze_duration 限制分析时长提高并发性能
+            ProcessBuilder pb0 = new ProcessBuilder(
+                    "ffprobe",
+                    "-v", "quiet",
+                    "-print_format", "json",
+                    "-show_format",
+                    "-show_streams",
+                    "-max_analyze_duration", "2000000", // 21TB高并发优化:最多分析2秒
+                    "-"                                 // 【核心】从 stdin 读取
+            );
+            ProcessBuilder pb = new ProcessBuilder(cmd);
+
+            Process process = pb.start();
+            // 3. 异步将 Java 锁定的文件流源源不断地抽给 FFmpeg 子进程
+            // 利用 JDK 21 的线程池或 Virtual Thread 异步搬运,防止 read/write 互相阻塞
+            CompletableFuture<Void> writeTask = CompletableFuture.runAsync(() -> {
+                try (OutputStream os = process.getOutputStream()) {
+                    byte[] buffer = new byte[64 * 1024]; // 64KB 缓冲区
+                    int len;
+                    // 注意:ffprobe 不需要读完整个 10GB 视频,它读够头部元数据就会关闭 stdin
+                    while ((len = fis.read(buffer)) != -1) {
+                        os.write(buffer, 0, len);
+                        os.flush();
+                    }
+                } catch (IOException e) {
+                    // FFprobe 读取足够数据后主动关闭 stdin 会触发 Broken pipe,属于正常现象,静默处理
+                }
+            });
+
+            // 4. 读取 FFmpeg 解析出来的 JSON 结果
+            String jsonResult;
+            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
+                jsonResult = reader.lines().collect(Collectors.joining("\n"));
+            }
+
+            // 5. 等待进程结束并清理
+            int exitCode = process.waitFor();
+            writeTask.join(); // 确保流关闭
+            if (exitCode != 0) {
+                String errorMsg = String.format("ffprobe 获取 %s 元数据失败,退出码: %s\n", file.getAbsolutePath(), exitCode);
+                throw new RuntimeException(errorMsg);
+            }
+            return FFmpegWrapper.parseAndGetMediaProps(jsonResult);
+        } catch (IOException | InterruptedException e) {
+            throw new RuntimeException("FFmpeg 交互异常", e);
+        }
+    }
+
     @Override
     public ImageInfo getImageInfo(String objectId, String absolutePath) {
         try {

+ 0 - 1
oss-store/src/main/java/cn/reghao/oss/store/util/UploadState.java

@@ -50,7 +50,6 @@ public class UploadState {
 
             this.mimeType = TIKA.detect(header);
             this.mimeDetected = true;
-            log.info("探测到文件类型: {}", this.mimeType);
         }
 
         int length = buf.readableBytes();