yangyb hace 1 mes
padre
commit
c355d5ae3a

+ 11 - 2
operating-service/pom.xml

@@ -46,7 +46,7 @@
         <dependency>
             <groupId>com.txz</groupId>
             <artifactId>operating-api</artifactId>
-            <version>1.0.1-SNAPSHOT</version>
+            <version>1.0.0-SNAPSHOT</version>
         </dependency>
 
         <!--test依赖-->
@@ -87,7 +87,16 @@
                 </exclusion>
             </exclusions>
         </dependency>
-
+        <dependency>
+            <groupId>io.minio</groupId>
+            <artifactId>minio</artifactId>
+            <version>8.2.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.5.13</version>
+        </dependency>
     </dependencies>
 
     <build>

+ 62 - 0
operating-service/src/main/java/com/txz/operating/config/minio/CustomMinioClient.java

@@ -0,0 +1,62 @@
+package com.txz.operating.config.minio;
+
+
+import com.google.common.collect.Multimap;
+import io.minio.*;
+import io.minio.errors.*;
+import io.minio.messages.Part;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+
+@Component
+public class CustomMinioClient extends MinioClient {
+
+    /**
+     * 继承父类
+     */
+    public CustomMinioClient(MinioClient client) {
+        super(client);
+    }
+
+    /**
+     * 初始化分片上传即获取uploadId
+     */
+    public String initMultiPartUpload(String bucket, String region, String object, Multimap<String, String> headers, Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
+        CreateMultipartUploadResponse response = this.createMultipartUpload(bucket, region, object, headers, extraQueryParams);
+        return response.result().uploadId();
+    }
+
+
+    /**
+     * 上传单个分片
+     */
+    public UploadPartResponse uploadMultiPart(String bucket, String region, String object, Object data,
+                                              int length,
+                                              String uploadId,
+                                              int partNumber,
+                                              Multimap<String, String> headers,
+                                              Multimap<String, String> extraQueryParams) throws IOException, InvalidKeyException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException {
+        return this.uploadPart(bucket, region, object, data, length, uploadId, partNumber, headers, extraQueryParams);
+    }
+
+    /**
+     * 合并分片
+     */
+    public ObjectWriteResponse mergeMultipartUpload(String bucketName, String region, String objectName, String uploadId, Part[] parts, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws IOException, NoSuchAlgorithmException, InsufficientDataException, ServerException, InternalException, XmlParserException, InvalidResponseException, ErrorResponseException, ServerException, InvalidKeyException {
+        return this.completeMultipartUpload(bucketName, region, objectName, uploadId, parts, extraHeaders, extraQueryParams);
+    }
+
+    public void cancelMultipartUpload(String bucketName, String region, String objectName, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws ServerException, InsufficientDataException, ErrorResponseException, NoSuchAlgorithmException, IOException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
+        this.abortMultipartUpload(bucketName, region, objectName, uploadId, extraHeaders, extraQueryParams);
+    }
+
+    /**
+     * 查询当前上传后的分片信息
+     */
+    public ListPartsResponse listMultipart(String bucketName, String region, String objectName, Integer maxParts, Integer partNumberMarker, String uploadId, Multimap<String, String> extraHeaders, Multimap<String, String> extraQueryParams) throws NoSuchAlgorithmException, InsufficientDataException, IOException, InvalidKeyException, ServerException, XmlParserException, ErrorResponseException, InternalException, InvalidResponseException {
+        return this.listParts(bucketName, region, objectName, maxParts, partNumberMarker, uploadId, extraHeaders, extraQueryParams);
+    }
+}

+ 148 - 0
operating-service/src/main/java/com/txz/operating/config/minio/FileController.java

@@ -0,0 +1,148 @@
+package com.txz.operating.config.minio;
+
+
+import cn.hutool.core.date.DateUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.txz.operating.result.Result;
+import io.minio.ObjectWriteResponse;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.apache.commons.compress.utils.IOUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServletResponse;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.util.Date;
+import java.util.List;
+
+@Api(tags = "文件操作接口")
+@RestController
+@RequestMapping(value = "/file")
+public class FileController {
+
+    @Autowired
+    MinioUtil minioUtil;
+
+    public static final String NORM_DAY_PATTERN = "yyyy/MM/dd";
+
+    @ApiOperation("上传一个文件")
+    @PostMapping(value = "/upload")
+    public Result fileUpload(@RequestParam MultipartFile file, @RequestParam String bucket,
+                             @RequestParam(required = false) String objectName) throws Exception {
+        String dir = DateUtil.format(new Date(), NORM_DAY_PATTERN)+ "/";
+//        String fileName = IdUtil.simpleUUID();
+        minioUtil.createBucket(bucket);
+        if (objectName != null) {
+            minioUtil.uploadFile(file.getInputStream(), bucket, objectName + "/" + file.getOriginalFilename());
+        } else {
+            minioUtil.uploadFile(file.getInputStream(), bucket, file.getOriginalFilename());
+        }
+        return Result.success();
+    }
+
+    @ApiOperation("列出所有的桶")
+    @GetMapping(value = "/listBucket")
+    public Result listBuckets() throws Exception {
+        return Result.success(minioUtil.listBuckets());
+    }
+
+    @ApiOperation("递归列出一个桶中的所有文件和目录")
+    @GetMapping(value = "/listFile")
+    public Result listFiles(@RequestParam String bucket) throws Exception {
+        return Result.success(minioUtil.listFiles(bucket));
+    }
+
+    @ApiOperation("下载一个文件")
+    @GetMapping(value = "/downloadFile")
+    public void downloadFile(@RequestParam String bucket, @RequestParam String objectName,
+                             HttpServletResponse response) throws Exception {
+        InputStream stream = minioUtil.download(bucket, objectName);
+        ServletOutputStream output = response.getOutputStream();
+        response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(objectName.substring(objectName.lastIndexOf("/") + 1), "UTF-8"));
+        response.setContentType("application/octet-stream");
+        response.setCharacterEncoding("UTF-8");
+        IOUtils.copy(stream, output);
+    }
+
+
+    @ApiOperation("删除一个文件")
+    @GetMapping(value = "/deleteFile")
+    public Result deleteFile(@RequestParam String bucket, @RequestParam String objectName) throws Exception {
+        minioUtil.deleteObject(bucket, objectName);
+        return Result.success();
+    }
+
+    @ApiOperation("删除一个桶")
+    @GetMapping(value = "/deleteBucket")
+    public Result deleteBucket(@RequestParam String bucket) throws Exception {
+        minioUtil.deleteBucket(bucket);
+        return Result.success();
+    }
+
+
+    @ApiOperation("复制一个文件")
+    @GetMapping("/copyObject")
+    public Result copyObject(@RequestParam String sourceBucket, @RequestParam String sourceObject, @RequestParam String targetBucket, @RequestParam String targetObject) throws Exception {
+        minioUtil.copyObject(sourceBucket, sourceObject, targetBucket, targetObject);
+        return Result.success();
+    }
+
+    @GetMapping("/getObjectInfo")
+    @ApiOperation("获取文件信息")
+    public Result getObjectInfo(@RequestParam String bucket, @RequestParam String objectName) throws Exception {
+        return Result.success(minioUtil.getObjectInfo(bucket, objectName));
+    }
+
+    @GetMapping("/getResignedObjectUrl")
+    @ApiOperation("获取一个连接以供下载")
+    public Result getPresignedObjectUrl(@RequestParam String bucket, @RequestParam String objectName, @RequestParam Integer expires) throws Exception {
+
+        return Result.success(minioUtil.getPresignedObjectUrl(bucket, objectName, expires));
+    }
+
+    @GetMapping("/listAllFile")
+    @ApiOperation("获取minio中所有的文件")
+    public Result listAllFile() throws Exception {
+        return Result.success(minioUtil.listAllFile());
+    }
+
+    /**
+     * 处理大文件上传请求,将文件分片上传并合并。
+     *
+     * @param uploadfile 上传的文件,类型为MultipartFile,包含文件的原始数据。
+     * @param bucket     存储文件的桶名称,类型为String,指定文件存储的目标桶。
+     * @return 返回一个JSON格式的字符串,包含合并后的文件对象名称和存储桶名称。
+     * @throws RuntimeException 如果在上传或合并过程中发生异常,则抛出运行时异常。
+     */
+    @PostMapping("/uploadLargeFile")
+    public String uploadLargeFile(@RequestParam MultipartFile uploadfile, @RequestParam String bucket) {
+        ObjectWriteResponse mergeResult;
+        try {
+            // 将上传的文件分片为多个输入流,便于后续分片上传
+            List<InputStream> inputStreams = minioUtil.splitFileToInputStreams(uploadfile);
+
+            // 获取上传分片的URL信息,包括原始文件名、分片数量和目标桶
+            String originalFilename = uploadfile.getOriginalFilename();
+            SplitFileDto splitFileDto = minioUtil.applyUploadPsiResult2Minio(originalFilename,
+                    inputStreams.size(), bucket);
+
+            // 将分片文件上传到MinIO服务器
+            minioUtil.upload(splitFileDto.getChunkUploadUrls(), inputStreams);
+
+            // 合并已上传的分片文件,完成整个文件的上传
+            mergeResult = minioUtil.mergeMultipartUpload(originalFilename, splitFileDto.getUploadId(), bucket);
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+
+        // 构造返回结果,包含合并后的文件对象名称和存储桶名称
+        JSONObject result = new JSONObject();
+        result.put("objectName", mergeResult.object());
+        result.put("bucketName", mergeResult.bucket());
+        return result.toJSONString();
+    }
+}

+ 19 - 0
operating-service/src/main/java/com/txz/operating/config/minio/FileInfo.java

@@ -0,0 +1,19 @@
+package com.txz.operating.config.minio;
+
+import lombok.Data;
+
+@Data
+public class FileInfo {
+
+	/**
+	 * 文件名
+	 */
+	String filename;
+
+	/**
+	 * 是否是目录
+	 */
+	Boolean directory;
+
+
+}

+ 28 - 0
operating-service/src/main/java/com/txz/operating/config/minio/MInioConfig.java

@@ -0,0 +1,28 @@
+package com.txz.operating.config.minio;
+
+import io.minio.MinioClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @author FrozenWatermelon
+ * @date 2020/9/10
+ */
+@Configuration
+public class MInioConfig {
+
+    @Value("${minio.url}")
+    private String url;
+    @Value("${minio.accessKey}")
+    private String accessKey;
+    @Value("${minio.secretKey}")
+    private String secretKey;
+
+    @Bean
+    public MinioClient getMinioClient() {
+        return MinioClient.builder().endpoint(url)
+                .credentials(accessKey, secretKey).build();
+    }
+}

+ 432 - 0
operating-service/src/main/java/com/txz/operating/config/minio/MinioUtil.java

@@ -0,0 +1,432 @@
+package com.txz.operating.config.minio;
+
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.HashMultimap;
+import io.minio.*;
+import io.minio.errors.*;
+import io.minio.http.Method;
+import io.minio.messages.Bucket;
+import io.minio.messages.Item;
+import io.minio.messages.Part;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.*;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+@Component
+public class MinioUtil {
+
+    @Resource
+    private CustomMinioClient minioClient;
+
+    /**
+     * 创建一个桶
+     */
+    public void createBucket(String bucket) throws Exception {
+        boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucket).build());
+        if (!found) {
+            minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucket).build());
+        }
+    }
+
+    /**
+     * 上传一个文件
+     */
+    public void uploadFile(InputStream stream, String bucket, String objectName) throws Exception {
+        minioClient.putObject(PutObjectArgs.builder().bucket(bucket).object(objectName)
+                .stream(stream, -1, 10485760).build());
+    }
+
+    /**
+     * 列出所有的桶
+     */
+    public List<String> listBuckets() throws Exception {
+        List<Bucket> list = minioClient.listBuckets();
+        List<String> names = new ArrayList<>();
+        list.forEach(b -> {
+            names.add(b.name());
+        });
+        return names;
+    }
+
+    /**
+     * 列出一个桶中的所有文件和目录
+     */
+    public List<FileInfo> listFiles(String bucket) throws Exception {
+        Iterable<Result<Item>> results = minioClient.listObjects(
+                ListObjectsArgs.builder().bucket(bucket).recursive(true).build());
+
+        List<FileInfo> infos = new ArrayList<>();
+        results.forEach(r -> {
+            FileInfo info = new FileInfo();
+            try {
+                Item item = r.get();
+                info.setFilename(item.objectName());
+                info.setDirectory(item.isDir());
+                infos.add(info);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        });
+        return infos;
+    }
+
+    /**
+     * 下载一个文件
+     */
+    public InputStream download(String bucket, String objectName) throws Exception {
+        InputStream stream = minioClient.getObject(
+                GetObjectArgs.builder().bucket(bucket).object(objectName).build());
+        return stream;
+    }
+
+    /**
+     * 删除一个桶
+     */
+    public void deleteBucket(String bucket) throws Exception {
+        minioClient.removeBucket(RemoveBucketArgs.builder().bucket(bucket).build());
+    }
+
+    /**
+     * 删除一个对象
+     */
+    public void deleteObject(String bucket, String objectName) throws Exception {
+        minioClient.removeObject(RemoveObjectArgs.builder().bucket(bucket).object(objectName).build());
+    }
+
+
+    /**
+     * 复制文件
+     *
+     * @Param: [sourceBucket, sourceObject, targetBucket, targetObject]
+     * @return: void
+     * @Author: MrFugui
+     * @Date: 2021/11/15
+     */
+    public void copyObject(String sourceBucket, String sourceObject, String targetBucket, String targetObject) throws Exception {
+        this.createBucket(targetBucket);
+        minioClient.copyObject(CopyObjectArgs.builder().bucket(targetBucket).object(targetObject)
+                .source(CopySource.builder().bucket(sourceBucket).object(sourceObject).build()).build());
+    }
+
+    /**
+     * 获取文件信息
+     *
+     * @Param: [bucket, objectName]
+     * @return: java.lang.String
+     * @Author: MrFugui
+     * @Date: 2021/11/15
+     */
+    public String getObjectInfo(String bucket, String objectName) throws Exception {
+
+        return minioClient.statObject(StatObjectArgs.builder().bucket(bucket).object(objectName).build()).toString();
+
+    }
+
+    /**
+     * 生成一个给HTTP GET请求用的presigned URL。浏览器/移动端的客户端可以用这个URL进行下载,即使其所在的存储桶是私有的。
+     *
+     * @Param: [bucketName, objectName, expires]
+     * @return: java.lang.String
+     * @Author: MrFugui
+     * @Date: 2021/11/15
+     */
+    public String getPresignedObjectUrl(String bucketName, String objectName, Integer expires) throws Exception {
+        GetPresignedObjectUrlArgs build = GetPresignedObjectUrlArgs
+                .builder().bucket(bucketName).object(objectName).expiry(expires).method(Method.GET).build();
+        return minioClient.getPresignedObjectUrl(build);
+    }
+
+    /**
+     * 获取minio中所有的文件
+     *
+     * @Param: []
+     * @return: java.util.List<boot.spring.domain.FileInfo>
+     * @Author: MrFugui
+     * @Date: 2021/11/15
+     */
+    public List<FileInfo> listAllFile() throws Exception {
+        List<String> list = this.listBuckets();
+        List<FileInfo> FileInfos = new ArrayList<>();
+        for (String bucketName : list) {
+            FileInfos.addAll(this.listFiles(bucketName));
+        }
+
+
+        return FileInfos;
+    }
+
+
+    public void splitFile() throws Exception {
+        long CHUNK_SIZE = 30 * 1024 * 1024;
+        // 将文件分片存储
+        String filePath = "D:\\xiazai\\18a7bc018f35471f927ae205301c9442.mp4";
+        File file = new File(filePath);
+        long fileSize = file.length();
+        int chunkCount = (int) Math.ceil((double) fileSize / CHUNK_SIZE);
+
+        try (FileInputStream fis = new FileInputStream(file)) {
+            byte[] buffer = new byte[(int) CHUNK_SIZE];
+            for (int i = 0; i < chunkCount; i++) {
+                String chunkFileName = filePath + ".part" + (i + 1);
+                try (FileOutputStream fos = new FileOutputStream(chunkFileName)) {
+                    int bytesRead = fis.read(buffer);
+                    fos.write(buffer, 0, bytesRead);
+                    System.out.println(chunkFileName);
+                }
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    public List<InputStream> splitFileToInputStreams(MultipartFile uploadfile) throws IOException {
+        long CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片的大小为5MB
+        List<InputStream> inputStreams = new ArrayList<>(); // 用于存储分片的 InputStream
+
+        try (BufferedInputStream bis = new BufferedInputStream(uploadfile.getInputStream())) {
+            byte[] buffer = new byte[(int) CHUNK_SIZE];
+            int bytesRead;
+            while ((bytesRead = bis.read(buffer)) != -1) {
+                byte[] chunkData = new byte[bytesRead];
+                System.arraycopy(buffer, 0, chunkData, 0, bytesRead);
+                InputStream chunkInputStream = new ByteArrayInputStream(chunkData);
+                inputStreams.add(chunkInputStream);
+            }
+        }
+
+        return inputStreams; // 返回分片的 InputStream 列表
+    }
+
+    /**
+     * 第二步,申请一个大文件上传
+     * 该函数用于申请一个大文件的分块上传,生成每个分块的预签名URL,并返回包含上传ID和分块URL列表的DTO对象。
+     *
+     * @param fileName   文件名,用于生成上传ID和预签名URL
+     * @param chunkCount 文件分块的总数,用于生成对应数量的预签名URL
+     * @param bucketName MinIO存储桶的名称,用于指定文件上传的目标存储桶
+     * @return SplitFileDto 包含上传ID和分块预签名URL列表的DTO对象
+     * @throws ServerException           服务器异常
+     * @throws InsufficientDataException 数据不足异常
+     * @throws ErrorResponseException    错误响应异常
+     * @throws IOException               IO异常
+     * @throws NoSuchAlgorithmException  无此算法异常
+     * @throws InvalidKeyException       无效密钥异常
+     * @throws XmlParserException        XML解析异常
+     * @throws InvalidResponseException  无效响应异常
+     * @throws InternalException         内部异常
+     */
+    public SplitFileDto applyUploadPsiResult2Minio(String fileName, Integer chunkCount, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
+        // 获取文件上传的唯一ID
+        String uploadId = getUploadId(fileName, bucketName);
+        SplitFileDto splitFileDto = new SplitFileDto();
+        Map<String, String> reqParams = new HashMap<>();
+        splitFileDto.setUploadId(uploadId);
+        reqParams.put("uploadId", uploadId);
+
+        // 生成每个分块的预签名URL
+        List<String> uploadUrlList = new ArrayList<>();
+        for (int i = 1; i <= chunkCount; i++) {
+            reqParams.put("partNumber", String.valueOf(i));
+            String uploadUrl = getPresignedObjectUrl(fileName, reqParams, bucketName);
+            uploadUrlList.add(uploadUrl);
+        }
+
+        // 设置分块预签名URL列表并返回DTO对象
+        splitFileDto.setChunkUploadUrls(uploadUrlList);
+        return splitFileDto;
+    }
+
+
+    /**
+     * 准备分片上传时,在此先获取上传任务id
+     */
+    private String getUploadId(String objectName, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
+        String contentType = "application/octet-stream";
+        HashMultimap<String, String> headers = HashMultimap.create();
+        headers.put("Content-Type", contentType);
+        return minioClient.initMultiPartUpload(bucketName, null, objectName, headers, null);
+    }
+
+
+    /**
+     * 获取分片上传的预签名URL。
+     * <p>
+     * 该方法通过MinIO客户端生成一个用于分片上传的预签名URL。预签名URL允许客户端在指定的时间内直接上传文件到指定的存储桶,而无需通过服务器进行身份验证。
+     *
+     * @param fileName   要上传的文件名,该文件名将作为对象存储在MinIO中。
+     * @param reqParams  额外的查询参数,这些参数将包含在生成的预签名URL中。
+     * @param bucketName 目标存储桶的名称,文件将被上传到该存储桶中。
+     * @return 返回一个预签名的URL,客户端可以使用该URL直接上传文件。
+     * @throws ServerException           如果与MinIO服务器的通信失败。
+     * @throws InsufficientDataException 如果从服务器接收的数据不完整。
+     * @throws ErrorResponseException    如果服务器返回错误响应。
+     * @throws IOException               如果发生I/O错误。
+     * @throws NoSuchAlgorithmException  如果请求的加密算法不可用。
+     * @throws InvalidKeyException       如果提供的密钥无效。
+     * @throws InvalidResponseException  如果服务器返回的响应无效。
+     * @throws XmlParserException        如果解析XML响应时发生错误。
+     * @throws InternalException         如果发生内部错误。
+     */
+    private String getPresignedObjectUrl(String fileName, Map<String, String> reqParams, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, InvalidResponseException, XmlParserException, InternalException {
+        // 使用MinIO客户端生成预签名URL,设置HTTP方法为PUT,并指定文件、存储桶、过期时间及额外查询参数
+        return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
+                .method(Method.PUT)
+                .bucket(bucketName)
+                .object(fileName)
+                .expiry(1, TimeUnit.DAYS)
+                .extraQueryParams(reqParams)
+                .build());
+    }
+
+
+    /**
+     * 将分片文件逐个上传到指定的URL。每个分片文件通过HTTP PUT请求直接上传到MinIO存储,避免通过服务端转发,减少网络IO开销。
+     * 在文件合并之前,分片文件可以重复覆盖上传。
+     *
+     * @param uploadUrlList 包含每个分片文件上传URL的列表,URL已签名。
+     * @param chunkFiles    包含每个分片文件输入流的列表,每个输入流对应一个分片文件。
+     * @throws IOException 如果在上传过程中发生IO错误,则抛出此异常。
+     */
+    public void upload(List<String> uploadUrlList, List<InputStream> chunkFiles) throws IOException {
+
+        // 创建默认的HTTP客户端,用于执行上传请求
+        CloseableHttpClient httpClient = HttpClients.createDefault();
+
+        // 遍历每个分片文件的上传URL和对应的输入流
+        for (int i = 0; i < uploadUrlList.size(); i++) {
+            // 创建HTTP PUT请求,将分片文件上传到指定的URL
+            String chunkUploadUrl = uploadUrlList.get(i);
+            HttpPut httpPut = new HttpPut(chunkUploadUrl);
+            httpPut.setHeader("Content-Type", "video/mp4");
+
+            // 生成随机的文件名,并设置Content-Disposition头
+            UUID uuid = UUID.randomUUID();
+            //todo 可以根据自己的文件加后缀
+            String name = uuid + ".mp4";
+            httpPut.addHeader("Content-Disposition", "filename=" + urlEncode(name, "UTF-8"));
+
+            // 将输入流转换为字节数组,并设置为请求体
+            byte[] chunkData = toByteArray(chunkFiles.get(i));
+            ByteArrayEntity byteArrayEntity = new ByteArrayEntity(chunkData);
+            httpPut.setEntity(byteArrayEntity);
+
+            // 执行上传请求,并获取响应
+            CloseableHttpResponse chunkUploadResp = httpClient.execute(httpPut);
+
+            // 打印上传响应信息
+            System.out.println("[分片" + (i + 1) + "]上传响应:" + JSON.toJSONString(chunkUploadResp));
+
+            // 释放连接资源
+            httpPut.releaseConnection();
+        }
+    }
+
+    /**
+     * 将输入流(InputStream)转换为字节数组(byte[])。
+     * <p>
+     * 该函数通过读取输入流中的数据,并将其写入到字节数组输出流(ByteArrayOutputStream)中,
+     * 最终将输出流的内容转换为字节数组返回。
+     *
+     * @param inputStream 要转换的输入流,不能为null。
+     * @return 包含输入流数据的字节数组。
+     * @throws IOException 如果读取输入流时发生I/O错误。
+     */
+    public static byte[] toByteArray(InputStream inputStream) throws IOException {
+        // 创建一个字节数组输出流,用于缓存从输入流读取的数据
+        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+        int nRead;
+        byte[] data = new byte[1024];
+
+        // 从输入流中读取数据,直到读取到流的末尾(返回-1)
+        while ((nRead = inputStream.read(data, 0, data.length)) != -1) {
+            // 将读取到的数据写入到字节数组输出流中
+            buffer.write(data, 0, nRead);
+        }
+
+        // 确保所有数据都已写入到输出流中
+        buffer.flush();
+
+        // 将字节数组输出流的内容转换为字节数组并返回
+        return buffer.toByteArray();
+    }
+
+    /**
+     * 对给定的字符串进行URL编码,并使用指定的字符编码。
+     * 编码过程中,会将字符串中的特殊字符替换为对应的百分号编码形式。
+     * 如果输入的字符串为null,则返回空字符串。
+     *
+     * @param value    需要编码的字符串,可以为null
+     * @param encoding 使用的字符编码,例如"UTF-8"
+     * @return 编码后的字符串,如果输入为null则返回空字符串
+     * @throws IllegalArgumentException 如果指定的字符编码不被支持
+     */
+    public static String urlEncode(String value, String encoding) {
+        if (value == null) {
+            return "";
+        }
+
+        try {
+            // 使用URLEncoder对字符串进行编码
+            String encoded = URLEncoder.encode(value, encoding);
+
+            // 替换编码后的字符串中的特定字符为对应的百分号编码形式
+            return encoded.replace("+", "%20").replace("*", "%2A").replace("~", "%7E").replace("/", "%2F");
+        } catch (UnsupportedEncodingException e) {
+            // 如果指定的字符编码不被支持,抛出IllegalArgumentException异常
+            throw new IllegalArgumentException();
+        }
+    }
+
+    /**
+     * 分片上传完后合并
+     * <p>
+     * 该函数用于在分片上传完成后,将所有分片合并为一个完整的对象。
+     *
+     * @param objectName 对象名称,即上传的文件名
+     * @param uploadId   上传任务的唯一标识符
+     * @param bucketName 存储桶名称,即文件存储的容器
+     * @return ObjectWriteResponse 对象写入响应,包含合并后的对象信息
+     * @throws ServerException           服务器异常
+     * @throws InsufficientDataException 数据不足异常
+     * @throws ErrorResponseException    错误响应异常
+     * @throws IOException               输入输出异常
+     * @throws NoSuchAlgorithmException  无此算法异常
+     * @throws InvalidKeyException       无效密钥异常
+     * @throws XmlParserException        XML解析异常
+     * @throws InvalidResponseException  无效响应异常
+     * @throws InternalException         内部异常
+     */
+    public ObjectWriteResponse mergeMultipartUpload(String objectName, String uploadId, String bucketName) throws ServerException, InsufficientDataException, ErrorResponseException, IOException, NoSuchAlgorithmException, InvalidKeyException, XmlParserException, InvalidResponseException, InternalException {
+        System.out.println("ready to merge <" + objectName + " - " + uploadId + " - " + bucketName + ">");
+
+        // 查询上传后的分片数据
+        ListPartsResponse partResult = minioClient.listMultipart(bucketName, null, objectName, 1000, 0, uploadId, null, null);
+        int chunkCount = partResult.result().partList().size();
+        Part[] parts = new Part[chunkCount];
+        int partNumber = 1;
+
+        // 将分片数据转换为合并所需的格式
+        for (Part part : partResult.result().partList()) {
+            parts[partNumber - 1] = new Part(partNumber, part.etag());
+            partNumber++;
+        }
+
+        // 合并分片并返回合并后的对象信息
+        ObjectWriteResponse objectWriteResponse = minioClient.mergeMultipartUpload(bucketName, null, objectName, uploadId, parts, null, null);
+        return objectWriteResponse;
+    }
+
+}

+ 23 - 0
operating-service/src/main/java/com/txz/operating/config/minio/SplitFileDto.java

@@ -0,0 +1,23 @@
+package com.txz.operating.config.minio;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * @Author: masiyi
+ * @Date: 2025/4/7
+ * @Describe:
+ */
+@Data
+public class SplitFileDto {
+
+    /**
+     * 上传id
+     */
+    private String uploadId;
+    /**
+     * 分片上传的url
+     */
+    private List<String> chunkUploadUrls;
+}

+ 1 - 1
operating-service/src/main/java/com/txz/operating/configurer/SwaggerConfig.java

@@ -47,7 +47,7 @@ public class SwaggerConfig {
                 .apiInfo(getApiInfo())
                 .select()
                 //设置basePackage会将包下的所有被@Api标记类的所有方法作为api
-                .apis(RequestHandlerSelectors.basePackage("com.txz.project.web"))
+                .apis(RequestHandlerSelectors.basePackage("com.txz.operating.controller"))
                 //只有标记了@ApiOperation的方法才会暴露出给swagger
                 .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                 .paths(PathSelectors.regex("/api/.*")).build();

+ 5 - 0
operating-service/src/main/resources/bootstrap.properties

@@ -59,3 +59,8 @@ management.endpoints.web.base-path=/${spring.application.name}
 management.endpoints.web.path-mapping.shutdown=/shutThisBoot
 management.server.address=127.0.0.1
 
+
+#minio
+minio.url= http://124.220.229.80:9098
+minio.accessKey= admin
+minio.secretKey= minio123