Redian新闻
>
SpringBoot 整合 MinIO 实现视频的分片上传/断点续传(亲测可行)

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自身对于临时上传的分片,会实施定时清理。

Demo地址

  • https://github.com/robinsyn/MinIO_Demo

来源:blog.csdn.net/weixin_44153131/

article/details/129249169

后端专属技术群

构建高质量的技术交流社群,欢迎从事编程开发、技术招聘HR进群,也欢迎大家分享自己公司的内推信息,相互帮助,一起进步!

文明发言,以交流技术职位内推行业探讨为主

广告人士勿入,切勿轻信私聊,防止被骗

加我好友,拉你进群

微信扫码关注该文公众号作者

戳这里提交新闻线索和高质量文章给我们。
相关阅读
SpringBoot 接口快速开发神器(接口可视化界面实现)SpringBoot + 规则引擎 URule,真的很强!和 if else说再见,SpringBoot 这样做参数校验才足够优雅!SpringBoot项目中使用缓存的正确姿势,太优雅了!SpringBoot 集成 Camunda 流程引擎,实现一套完整的业务流程【𝐂𝐚𝐥𝐧𝐢𝐊𝐞𝐚𝐧双皮奶无痕内裤】49元三条!巨巨巨好穿 !!简直就是辣妹顶配,食品级冰箱收纳盒【一日团】SpringBoot 通用限流方案(VIP珍藏版)SpringBoot 调用外部接口的三种方式公司新入职一位大佬,把SpringBoot项目启动时间从7分钟降到了40秒!五一杭州,闹中取静,俯瞰西湖(亲测线路,建议收藏)基于 SpringBoot 实现多租户架构:支持应用多租户部署和管理SpringBoot+Mybatis 如何实现流式查询,你知道吗?实测可行,金葵花免管理费入 土 为 安Vue+SpringBoot 集成 PageOffice 实现在线编辑Word、excel文档三分钟了解 SpringBoot 的启动流程护养一身阳气,远离疾病困扰之一Spring Boot 整合分布式消息平台 PulsarSpringBoot 插件化开发模式,强烈推荐!SpringBoot 中的自带工具类,开发效率增加一倍!快试试用 API Key 来保护你的 SpringBoot 接口安全吧第九十九章 破案Spring Boot + 规则引擎Drools,强!SpringBoot 中的自带工具类,开发效率倍增!第一百章 转战MacBook Pro 13in 2018 A1989 keep rebooting ( screen/battery/otheSpring循环依赖那些事儿(含Spring详细流程图)Corsair DOMINATOR PLATINUM RGB 64GB (2x32GB) DDR5 DRAM 5200MHzSpring Boot + 规则引擎 URule,太强了!护养一身阳气,远离疾病困扰之二数据可视化:基于 Echarts + SpringBoot 的动态实时大屏银行监管系统【源码】SpringBoot 集成 EasyExcel 3.x 优雅实现 Excel 导入导出𝐂𝐚𝐥𝐧𝐢𝐊𝐞𝐚𝐧双皮奶内衣裤,软弹有度,上身0束缚~SpringBoot 快速实现IP地址解析6种方式读取Springboot的配置
logo
联系我们隐私协议©2024 redian.news
Redian新闻
Redian.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Redian.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。