springboot整合MinIO实现视频的分片上传/断点续传

1、前言

之前做了一个慕课网上的仿短视频开发,里面有很多比较粗糙的实现,比如视频上传部分是直接由前端上传云服务,没考虑到客户的网络环境质量等问题,如果一个视频快上传完了,但是网断了没有上传完成需要客户重新上传,这对于用户体验是极差的。

那么我们对于视频文件的上传可以采取断点续传,上传过程中,如果出现网络异常或程序崩溃导致文件上传失败时,将从断点记录处继续上传未上传完成的部分,断点续传依赖于MD5和分片上传,对于本demo分片上传的流程如图
在这里插入图片描述
通过文件唯一标识MD5,在数据库中查询此前是否创建过该SysUploadTask,如果存在,直接返回TaskInfo;如果不存在,通过amazonS3获取到UploadId并新建一个SysUploadTask返回。前端将文件分好片后,通过服务器得到每一片的一个预地址,然后由前端直接向minio服务器发起真正的上传请求,避免上传时占用应用服务器的带宽,影响系统稳定。最后再向后端服务器发起合并请求。

2、数据库结构

在这里插入图片描述

3、后端实现

3.1、根据MD5获取是否存在相同文件

Controller层

    /**
     * 查询是否上传过,若存在,返回TaskInfoDTO
     * @param identifier 文件md5
     * @return
     */
    @GetMapping("/{identifier}")
    public GraceJSONResult taskInfo (@PathVariable("identifier") String identifier) {
        return GraceJSONResult.ok(sysUploadTaskService.getTaskInfo(identifier));
    }

Service层

    /**
     * 查询是否上传过,若存在,返回TaskInfoDTO
     * @param identifier
     * @return
     */
    public TaskInfoDTO getTaskInfo(String identifier) {
        SysUploadTask task = getByIdentifier(identifier);
        if (task == null) {
            return null;
        }
        TaskInfoDTO result = new TaskInfoDTO().setFinished(true).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(task.getBucketName(), task.getObjectKey()));

        boolean doesObjectExist = amazonS3.doesObjectExist(task.getBucketName(), task.getObjectKey());
        if (!doesObjectExist) {
            // 未上传完,返回已上传的分片
            ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getObjectKey(), task.getUploadId());
            PartListing partListing = amazonS3.listParts(listPartsRequest);
            result.setFinished(false).getTaskRecord().setExitPartList(partListing.getParts());
        }
        return result;
    }

3.2、初始化一个上传任务

Controller层

    /**
     * 创建一个上传任务
     * @return
     */
    @PostMapping
    public GraceJSONResult initTask (@Valid @RequestBody InitTaskParam param) {
        return GraceJSONResult.ok(sysUploadTaskService.initTask(param));
    }

Service层

	/**
     * 初始化一个任务
     */
    public TaskInfoDTO initTask(InitTaskParam param) {

        Date currentDate = new Date();
        String bucketName = minioProperties.getBucketName();
        String fileName = param.getFileName();
        String suffix = fileName.substring(fileName.lastIndexOf(".")+1, fileName.length());
        String key = StrUtil.format("{}/{}.{}", DateUtil.format(currentDate, "YYYY-MM-dd"), IdUtil.randomUUID(), suffix);
        String contentType = MediaTypeFactory.getMediaType(key).orElse(MediaType.APPLICATION_OCTET_STREAM).toString();
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentType(contentType);
        InitiateMultipartUploadResult initiateMultipartUploadResult = amazonS3
                .initiateMultipartUpload(new InitiateMultipartUploadRequest(bucketName, key).withObjectMetadata(objectMetadata));
        String uploadId = initiateMultipartUploadResult.getUploadId();

        SysUploadTask task = new SysUploadTask();
        int chunkNum = (int) Math.ceil(param.getTotalSize() * 1.0 / param.getChunkSize());
        task.setBucketName(minioProperties.getBucketName())
                .setChunkNum(chunkNum)
                .setChunkSize(param.getChunkSize())
                .setTotalSize(param.getTotalSize())
                .setFileIdentifier(param.getIdentifier())
                .setFileName(fileName)
                .setObjectKey(key)
                .setUploadId(uploadId);
        sysUploadTaskMapper.insert(task);
        return new TaskInfoDTO().setFinished(false).setTaskRecord(TaskRecordDTO.convertFromEntity(task)).setPath(getPath(bucketName, key));
    }

3.3、获取每个分片的预签名上传地址

Controller层

    /**
     * 获取每个分片的预签名上传地址
     * @param identifier
     * @param partNumber
     * @return
     */
    @GetMapping("/{identifier}/{partNumber}")
    public GraceJSONResult preSignUploadUrl (@PathVariable("identifier") String identifier, @PathVariable("partNumber") Integer partNumber) {
        SysUploadTask task = sysUploadTaskService.getByIdentifier(identifier);
        if (task == null) {
            return GraceJSONResult.error("分片任务不存在");
        }
        Map<String, String> params = new HashMap<>();
        params.put("partNumber", partNumber.toString());
        params.put("uploadId", task.getUploadId());
        return GraceJSONResult.ok(sysUploadTaskService.genPreSignUploadUrl(task.getBucketName(), task.getObjectKey(), params));
    }

Service层

    /**
     * 生成预签名上传url
     * @param bucket 桶名
     * @param objectKey 对象的key
     * @param params 额外的参数
     * @return
     */
    public String genPreSignUploadUrl(String bucket, String objectKey, Map<String, String> params) {
        Date currentDate = new Date();
        Date expireDate = DateUtil.offsetMillisecond(currentDate, PRE_SIGN_URL_EXPIRE.intValue());
        GeneratePresignedUrlRequest request = new GeneratePresignedUrlRequest(bucket, objectKey)
                .withExpiration(expireDate).withMethod(HttpMethod.PUT);
        if (params != null) {
            params.forEach((key, val) -> request.addRequestParameter(key, val));
        }
        URL preSignedUrl = amazonS3.generatePresignedUrl(request);
        return preSignedUrl.toString();
    }

3.4、合并分片

Controller层

    /**
     * 合并分片
     * @param identifier
     * @return
     */
    @PostMapping("/merge/{identifier}")
    public GraceJSONResult merge (@PathVariable("identifier") String identifier) {
        sysUploadTaskService.merge(identifier);
        return GraceJSONResult.ok();
    }

Service层

    /**
     * 合并分片
     * @param identifier
     */
    public void merge(String identifier) {
        SysUploadTask task = getByIdentifier(identifier);
        if (task == null) {
            throw new RuntimeException("分片任务不存");
        }

        ListPartsRequest listPartsRequest = new ListPartsRequest(task.getBucketName(), task.getObjectKey(), task.getUploadId());
        PartListing partListing = amazonS3.listParts(listPartsRequest);
        List<PartSummary> parts = partListing.getParts();
        if (!task.getChunkNum().equals(parts.size())) {
            // 已上传分块数量与记录中的数量不对应,不能合并分块
            throw new RuntimeException("分片缺失,请重新上传");
        }
        CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest()
                .withUploadId(task.getUploadId())
                .withKey(task.getObjectKey())
                .withBucketName(task.getBucketName())
                .withPartETags(parts.stream().map(partSummary -> new PartETag(partSummary.getPartNumber(), partSummary.getETag())).collect(Collectors.toList()));
        CompleteMultipartUploadResult result = amazonS3.completeMultipartUpload(completeMultipartUploadRequest);
    }

4、分片文件清理问题

视频上传一半不上传了,怎么清理碎片分片。

可以考虑在sys_upload_task表中新加一个status字段,表示是否合并分片,默认为false,merge请求结束后变更为true,通过一个定时任务定期清理为status为false的记录。另外MinIO自身对于临时上传的分片,会实施定时清理。

5、Demo地址

springboot整合MinIO实现视频的分片上传/断点续传

  • 3
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Spring Boot实现MinIO文件服务器的分片上传断点续传,可以使用MinIO Java SDK提供的API和Spring Boot提供的Multipart File上传功能。具体实现步骤如下: 1. 引入MinIO Java SDK和Spring Boot的依赖 ```xml <dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.0.4</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> ``` 2. 配置MinIO客户端 在Spring Boot的配置文件中,配置MinIO客户端的连接信息。 ```yaml minio: endpoint: minio.example.com access-key: ACCESS_KEY secret-key: SECRET_KEY secure: false ``` 3. 初始化Multipart上传上传大文件之前,使用 `InitiateMultipartUpload` API 方法初始化一个Multipart上传会话,并获取一个上传ID。上传ID用于标识一个Multipart上传会话。 ```java import io.minio.MinioClient; import io.minio.errors.MinioException; import io.minio.messages.Part; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.ArrayList; import java.util.List; @Service public class MinioService { @Autowired private MinioClient minioClient; @Value("${minio.bucket-name}") private String bucketName; public String initiateMultipartUpload(String objectName) throws MinioException { String uploadId = minioClient.initiateMultipartUpload(bucketName, objectName).uploadId(); return uploadId; } } ``` 4. 上传文件块 将大文件分成多个小块,每个小块的大小可以根据需求自定义。使用 `UploadPart` API 方法将每个小块独立上传上传时需要指定上传的文件名、块编号、块大小以及上传ID等信息。 ```java public class MinioService { // ... public List<Part> uploadParts(String objectName, String uploadId, MultipartFile file, int partSize) throws IOException, MinioException { List<Part> parts = new ArrayList<>(); int partNumber = 1; byte[] buffer = new byte[partSize]; while (true) { int bytesRead = file.getInputStream().read(buffer); if (bytesRead < 0) { break; } byte[] partData = new byte[bytesRead]; System.arraycopy(buffer, 0, partData, 0, bytesRead); Part part = minioClient.uploadPart( bucketName, objectName, partNumber, uploadId, partData, bytesRead); parts.add(part); partNumber++; } return parts; } } ``` 5. 完成Multipart上传 上传所有文件块后,使用 `CompleteMultipartUpload` API 方法将它们合并成一个完整的文件,最终得到上传的文件。 ```java public class MinioService { // ... public void completeMultipartUpload(String objectName, String uploadId, List<Part> parts) throws MinioException { minioClient.completeMultipartUpload(bucketName, objectName, uploadId, parts); } } ``` 6. 断点续传 如果上传中断,可以使用 `ListParts` API 方法获取已上传的文件块信息,然后从中断处继续上传。 ```java public class MinioService { // ... public List<Part> listParts(String objectName, String uploadId) throws MinioException { return minioClient.listParts(bucketName, objectName, uploadId).getParts(); } } ``` 使用Spring Boot的Multipart File上传功能上传文件时,需要在Controller中添加 `@RequestParam("file") MultipartFile file` 注解,自动将文件转换为MultipartFile类型。 ```java @RestController @RequestMapping("/file") public class FileController { @Autowired private MinioService minioService; @PostMapping("/upload") public void uploadFile(@RequestParam("file") MultipartFile file) throws IOException, MinioException { // 获取文件名和文件大小 String fileName = file.getOriginalFilename(); long fileSize = file.getSize(); // 定义块大小(5MB) int partSize = 5 * 1024 * 1024; // 初始化Multipart上传会话 String uploadId = minioService.initiateMultipartUpload(fileName); // 上传文件块 List<Part> parts = minioService.uploadParts(fileName, uploadId, file, partSize); // 完成Multipart上传 minioService.completeMultipartUpload(fileName, uploadId, parts); } @PostMapping("/resume") public void resumeUpload(@RequestParam("file") MultipartFile file, String objectName, String uploadId) throws IOException, MinioException { // 获取已上传的文件块信息 List<Part> parts = minioService.listParts(objectName, uploadId); // 上传文件块 parts = minioService.uploadParts(objectName, uploadId, file, partSize); // 完成Multipart上传 minioService.completeMultipartUpload(objectName, uploadId, parts); } } ``` 以上是使用Spring Boot实现MinIO文件服务器的分片上传断点续传的基本步骤,具体实现还需要根据实际需求进行调整。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值