|
|
@@ -1,15 +1,26 @@
|
|
|
package com.media.util.upload;
|
|
|
|
|
|
-import com.media.service.FileUploadService;
|
|
|
+import com.base.utils.Result;
|
|
|
+import com.media.domain.dto.MergeDto;
|
|
|
+import com.media.domain.po.MediaFile;
|
|
|
+import com.media.service.IMediaFileService;
|
|
|
import com.media.util.enums.StorageBucket;
|
|
|
-import io.minio.MinioClient;
|
|
|
-import io.minio.PutObjectArgs;
|
|
|
+import io.minio.*;
|
|
|
+import io.minio.errors.*;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
|
+import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
|
+import java.security.InvalidKeyException;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static com.media.util.enums.StorageBucket.VIDEO_FILES;
|
|
|
+
|
|
|
@Slf4j
|
|
|
@Component
|
|
|
public class MinioFileUploader {
|
|
|
@@ -17,65 +28,236 @@ public class MinioFileUploader {
|
|
|
private MinioClient minioClient;
|
|
|
@Value("${minio.endpoint}")
|
|
|
private String endpoint;
|
|
|
+ @Autowired
|
|
|
+ private IMediaFileService mediaFileService;
|
|
|
+
|
|
|
+ public String uploadFile(StorageBucket bucket, String objectName, InputStream inputStream, String contentType) {
|
|
|
+ try {
|
|
|
+ // 检查并创建存储桶
|
|
|
+ checkAndCreateBucket(bucket.getBucketName());
|
|
|
+
|
|
|
+ // 简化版上传,适用于3M大小的文件
|
|
|
+ minioClient.putObject(
|
|
|
+ PutObjectArgs.builder()
|
|
|
+ .bucket(bucket.getBucketName())
|
|
|
+ .object(objectName)
|
|
|
+ .stream(inputStream, inputStream.available(), -1)
|
|
|
+ .contentType(contentType)
|
|
|
+ .build()
|
|
|
+ );
|
|
|
|
|
|
-public String uploadFile(StorageBucket bucket, String objectName, InputStream inputStream, String contentType) {
|
|
|
- try {
|
|
|
- // 检查并创建存储桶
|
|
|
- checkAndCreateBucket(bucket.getBucketName());
|
|
|
-
|
|
|
- // 简化版上传,适用于3M大小的文件
|
|
|
- minioClient.putObject(
|
|
|
- PutObjectArgs.builder()
|
|
|
- .bucket(bucket.getBucketName())
|
|
|
- .object(objectName)
|
|
|
- .stream(inputStream, inputStream.available(), -1)
|
|
|
- .contentType(contentType)
|
|
|
- .build()
|
|
|
- );
|
|
|
-
|
|
|
- return generateFileUrl(bucket, objectName);
|
|
|
- } catch (Exception e) {
|
|
|
- throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
|
|
|
+ return generateFileUrl(bucket, objectName);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("文件上传失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void checkAndCreateBucket(String bucketName) {
|
|
|
+ try {
|
|
|
+ // 统一转换为小写,避免MinIO桶名兼容问题
|
|
|
+ String lowerCaseBucketName = bucketName.toLowerCase();
|
|
|
+ boolean exists = minioClient.bucketExists(
|
|
|
+ BucketExistsArgs.builder().bucket(lowerCaseBucketName).build()
|
|
|
+ );
|
|
|
+ if (!exists) {
|
|
|
+ // 创建存储桶
|
|
|
+ minioClient.makeBucket(
|
|
|
+ MakeBucketArgs.builder()
|
|
|
+ .bucket(lowerCaseBucketName)
|
|
|
+ .build()
|
|
|
+ );
|
|
|
+ log.info("存储桶创建成功: {}", lowerCaseBucketName);
|
|
|
+
|
|
|
+ // 设置存储桶策略为公开读取
|
|
|
+ setBucketPolicy(lowerCaseBucketName);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("存储桶操作失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-private void checkAndCreateBucket(String bucketName) {
|
|
|
- try {
|
|
|
- boolean exists = minioClient.bucketExists(
|
|
|
- io.minio.BucketExistsArgs.builder().bucket(bucketName).build()
|
|
|
- );
|
|
|
- if (!exists) {
|
|
|
- // 创建存储桶时设置为公开读取
|
|
|
- minioClient.makeBucket(
|
|
|
- io.minio.MakeBucketArgs.builder()
|
|
|
- .bucket(bucketName)
|
|
|
- .build()
|
|
|
+
|
|
|
+ private void setBucketPolicy(String bucketName) {
|
|
|
+ try {
|
|
|
+ // 存储桶策略配置(适配小写桶名)
|
|
|
+ String policyConfig = "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"s3:GetObject\"],\"Resource\":\"arn:aws:s3:::" + bucketName + "/*\"}]}";
|
|
|
+ minioClient.setBucketPolicy(
|
|
|
+ SetBucketPolicyArgs.builder()
|
|
|
+ .bucket(bucketName)
|
|
|
+ .config(policyConfig)
|
|
|
+ .build()
|
|
|
);
|
|
|
- // 设置存储桶策略为公开读取
|
|
|
- setBucketPolicy(bucketName);
|
|
|
+ log.info("存储桶公开读取策略设置成功: {}", bucketName);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("设置存储桶策略失败: {}", bucketName, e);
|
|
|
}
|
|
|
- } catch (Exception e) {
|
|
|
- throw new RuntimeException("存储桶操作失败: " + e.getMessage(), e);
|
|
|
}
|
|
|
-}
|
|
|
-
|
|
|
-private void setBucketPolicy(String bucketName) {
|
|
|
- try {
|
|
|
- // 设置存储桶策略为公开读取
|
|
|
- minioClient.setBucketPolicy(
|
|
|
- io.minio.SetBucketPolicyArgs.builder()
|
|
|
- .bucket(bucketName)
|
|
|
- .config("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"*\"},\"Action\":[\"s3:GetObject\"],\"Resource\":\"arn:aws:s3:::" + bucketName + "/*\"}]}\n")
|
|
|
- .build()
|
|
|
- );
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("设置存储桶策略失败: {}", bucketName, e);
|
|
|
+
|
|
|
+ private String generateFileUrl(StorageBucket bucket, String objectName) {
|
|
|
+ // 拼接URL时使用小写桶名
|
|
|
+ String lowerCaseBucketName = bucket.getBucketName().toLowerCase();
|
|
|
+ return endpoint + "/" + lowerCaseBucketName + "/" + objectName;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 文件是否存在
|
|
|
+ public boolean checkObjectExists(StorageBucket bucket, String objectName) {
|
|
|
+ try {
|
|
|
+ String lowerCaseBucketName = bucket.getBucketName().toLowerCase();
|
|
|
+ minioClient.statObject(
|
|
|
+ StatObjectArgs.builder()
|
|
|
+ .bucket(lowerCaseBucketName)
|
|
|
+ .object(objectName)
|
|
|
+ .build()
|
|
|
+ );
|
|
|
+ return true;
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 捕获不存在的异常,返回false
|
|
|
+ if (e instanceof ErrorResponseException && ((ErrorResponseException) e).errorResponse().code().equals("NoSuchKey")) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ log.warn("检查文件存在性异常: {}", objectName, e);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
+ public void mergeFile(MergeDto mergeDto) {
|
|
|
+ log.info("开始合并文件: {}", mergeDto.getFileKey());
|
|
|
+ boolean exists1 = checkObjectExists(VIDEO_FILES, mergeDto.getFileKey());
|
|
|
+ if (exists1) {
|
|
|
+ log.info("文件" + mergeDto.getFileKey() + "已存在,无需重复上传");
|
|
|
+ saveFile(mergeDto);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ // 关键修复1:先检查并创建合并所需的2个存储桶(转为小写)
|
|
|
+ String tempBucketName = StorageBucket.Video_Temp.getBucketName().toLowerCase();
|
|
|
+ String targetBucketName = StorageBucket.VIDEO_FILES.getBucketName().toLowerCase();
|
|
|
+ checkAndCreateBucket(tempBucketName); // 分块存储桶
|
|
|
+ checkAndCreateBucket(targetBucketName); // 合并后存储桶
|
|
|
|
|
|
+ // 1. 获取分块文件列表
|
|
|
+ List<ComposeSource> sourceList = getChunkSources(mergeDto, tempBucketName);
|
|
|
|
|
|
- private String generateFileUrl(StorageBucket bucket, String objectName) {
|
|
|
- return endpoint + "/" + bucket.getBucketName() + "/" + objectName;
|
|
|
+ // 校验分块列表是否为空
|
|
|
+ if (sourceList.isEmpty()) {
|
|
|
+ throw new RuntimeException("分块文件列表为空,无法合并: " + mergeDto.getFileKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 执行合并操作
|
|
|
+ String mergedObjectName = executeMerge(mergeDto, sourceList, targetBucketName);
|
|
|
+
|
|
|
+ // 3. 删除分块文件
|
|
|
+ deleteChunkFiles(mergeDto, tempBucketName);
|
|
|
+
|
|
|
+ log.info("文件合并完成: {} -> {}", mergeDto.getFileKey(), mergedObjectName);
|
|
|
+ saveFile(mergeDto);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("合并文件失败: {}", mergeDto.getFileKey(), e);
|
|
|
+ throw new RuntimeException("文件合并失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public void saveFile(MergeDto mergeDto) {
|
|
|
+ log.info("开始保存文件: {}", mergeDto.getFileKey());
|
|
|
+ MediaFile mediaFile = new MediaFile();
|
|
|
+ mediaFile.setOriginalName(mergeDto.getFileName());
|
|
|
+ mediaFile.setFileName(mergeDto.getFileKey());
|
|
|
+ mediaFile.setFileType(mergeDto.getContentType());
|
|
|
+ mediaFile.setFileSize((long) (mergeDto.getTotalChunk()*1024*1024*5));
|
|
|
+ mediaFile.setBucketName(StorageBucket.VIDEO_FILES.getBucketName());
|
|
|
+ mediaFile.setFileUrl(generateFileUrl(StorageBucket.VIDEO_FILES, mergeDto.getFileKey() + "." + getFileExtension(mergeDto.getContentType())));
|
|
|
+ mediaFile.setStorageType("minio");
|
|
|
+ mediaFile.setStatus(0);
|
|
|
+ mediaFile.setMd5(mergeDto.getFileKey());
|
|
|
+ mediaFileService.save(mediaFile);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取分块文件源列表(修复分块索引从0开始)
|
|
|
+ */
|
|
|
+ private List<ComposeSource> getChunkSources(MergeDto mergeDto, String tempBucketName) {
|
|
|
+ try {
|
|
|
+ List<ComposeSource> sourceList = new ArrayList<>();
|
|
|
+ int totalChunk = mergeDto.getTotalChunk();
|
|
|
+
|
|
|
+ // 关键修复2:分块索引从0开始,遍历所有分块
|
|
|
+ for (int i = 1; i <= totalChunk; i++) {
|
|
|
+ String chunkObjectName = "temp/" + mergeDto.getFileKey() + "/" + i;
|
|
|
+ log.debug("添加分块文件到合并列表: {}", chunkObjectName);
|
|
|
+
|
|
|
+ // 可选:检查分块是否存在,避免合并缺失
|
|
|
+ if (!checkObjectExists(StorageBucket.Video_Temp, chunkObjectName)) {
|
|
|
+ throw new RuntimeException("分块文件不存在: " + chunkObjectName);
|
|
|
+ }
|
|
|
+
|
|
|
+ ComposeSource source = ComposeSource.builder()
|
|
|
+ .bucket(tempBucketName)
|
|
|
+ .object(chunkObjectName)
|
|
|
+ .build();
|
|
|
+ sourceList.add(source);
|
|
|
+ }
|
|
|
+
|
|
|
+ return sourceList;
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new RuntimeException("获取分块文件列表失败: " + e.getMessage(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行文件合并操作
|
|
|
+ */
|
|
|
+
|
|
|
+ private String executeMerge(MergeDto mergeDto, List<ComposeSource> sourceList, String targetBucketName)
|
|
|
+ throws IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException,
|
|
|
+ ErrorResponseException, InsufficientDataException, InternalException, InvalidResponseException, ServerException {
|
|
|
+ // 目标文件名使用 MD5,格式为 md5/md5.格式
|
|
|
+ String fileKey = mergeDto.getFileKey();
|
|
|
+ String fileExtension = getFileExtension(mergeDto.getContentType());
|
|
|
+
|
|
|
+ String targetObjectName = fileKey +"." +fileExtension;
|
|
|
+
|
|
|
+ log.debug("开始合并文件到存储桶: {}, 目标对象名: {}", targetBucketName, targetObjectName);
|
|
|
+
|
|
|
+ ComposeObjectArgs composeArgs = ComposeObjectArgs.builder()
|
|
|
+ .bucket(targetBucketName)
|
|
|
+ .object(targetObjectName)
|
|
|
+ .sources(sourceList)
|
|
|
+ .build();
|
|
|
+ String etag = minioClient.composeObject(composeArgs).toString();
|
|
|
+ log.debug("合并操作完成,ETag: {}", etag);
|
|
|
+ return targetObjectName;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getFileExtension(String contentType) {
|
|
|
+ if (contentType == null || !contentType.contains("/")) {
|
|
|
+ return "unknown"; // 默认扩展名
|
|
|
+ }
|
|
|
+
|
|
|
+ // 提取 "video/mp4" 中的 "mp4" 部分
|
|
|
+ String[] parts = contentType.split("/");
|
|
|
+ if (parts.length >= 2) {
|
|
|
+ return parts[1]; // 返回类型部分,如 mp4, avi, mov 等
|
|
|
+ }
|
|
|
+
|
|
|
+ return "unknown";
|
|
|
+ }
|
|
|
+ /**
|
|
|
+ * 删除分块文件
|
|
|
+ */
|
|
|
+ private void deleteChunkFiles(MergeDto mergeDto, String tempBucketName) {
|
|
|
+ int totalChunk = mergeDto.getTotalChunk();
|
|
|
+ for (int i = 1; i <= totalChunk; i++) { // 同步从0开始遍历
|
|
|
+ String chunkObjectName = "temp/" + mergeDto.getFileKey() + "/" + i;
|
|
|
+ try {
|
|
|
+ minioClient.removeObject(
|
|
|
+ RemoveObjectArgs.builder()
|
|
|
+ .bucket(tempBucketName)
|
|
|
+ .object(chunkObjectName)
|
|
|
+ .build()
|
|
|
+ );
|
|
|
+ log.debug("分块文件已删除: {}", chunkObjectName);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("删除分块文件失败: {}", chunkObjectName, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
-}
|
|
|
+}
|