Spring Boot 集成阿里云和腾讯云实现分片上传

前端Demo(html文件)


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>分片上传测试</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js"></script>
</head>
<body>
    <h2>分片上传视频</h2>
    <input type="file" id="fileInput" />
    <button onclick="handleUpload()">上传视频</button>

    <div id="progress-container" style="margin-top: 20px; width: 100%; background-color: #f3f3f3; height: 20px; border-radius: 5px; overflow: hidden;">
        <div id="progress-bar" style="width: 0%; height: 100%; background-color: #4caf50;"></div>
        <span id="progress-text">0%</span>
    </div>

    <script>
        const CHUNK_SIZE = 5 * 1024 * 1024; // 每个分片5MB
        const uploadProgress = document.getElementById('progress-bar');
        const progressText = document.getElementById('progress-text');

        // 处理文件上传
        async function handleUpload() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            if (!file) {
                alert('请选择一个文件!');
                return;
            }

            const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
            let chunkIndex = 0;

            // 上传文件的每个分片
            for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
                const start = chunkIndex * CHUNK_SIZE;
                const end = Math.min(start + CHUNK_SIZE, file.size);
                const chunk = file.slice(start, end); // 获取文件片段

                // 计算分片的 MD5
                const md5 = await calculateChunkMD5(chunk);

                // 创建FormData并上传分片
                const formData = new FormData();
                formData.append('videoFile', chunk, file.name);
                formData.append('chunkIndex', chunkIndex);
                formData.append('totalChunks', totalChunks);
                formData.append('fileName', file.name);
                formData.append('md5', md5);  // 添加 MD5 值

                try {
                    await uploadChunk(formData, chunkIndex + 1, totalChunks);
                    updateProgress((chunkIndex + 1) / totalChunks);
                } catch (error) {
                    console.error('分片上传失败: ', error);
                    alert('上传失败,请检查控制台日志。');
                    break;
                }
            }

            alert('视频上传成功!');
        }

        // 上传单个分片
        async function uploadChunk(formData, chunkIndex, totalChunks) {
            return new Promise((resolve, reject) => {
                const xhr = new XMLHttpRequest();
                xhr.open('POST', 'http://localhost:7081/admin/api/courses/uploadVideo', true);

                xhr.onload = function () {
                    if (xhr.status === 200) {
                        console.log(`分片 ${chunkIndex}/${totalChunks} 上传成功`);
                        resolve();
                    } else {
                        console.error(`分片 ${chunkIndex}/${totalChunks} 上传失败, 状态码: `, xhr.status);
                        console.error('响应: ', xhr.responseText);  // 打印服务器响应
                        reject(new Error(`上传失败,状态码: ${xhr.status}`));
                    }
                };

                xhr.onerror = function () {
                    console.error(`分片 ${chunkIndex}/${totalChunks} 上传时发生错误`);
                    reject(new Error('网络错误,无法连接服务器'));
                };

                xhr.send(formData);
            });
        }

        // 计算每个分片的 MD5
        async function calculateChunkMD5(chunk) {
            return new Promise((resolve) => {
                const reader = new FileReader();
                reader.onload = (e) => {
                    const spark = new SparkMD5.ArrayBuffer();
                    spark.append(e.target.result);
                    const md5 = spark.end();
                    resolve(md5);
                };
                reader.readAsArrayBuffer(chunk);
            });
        }

        // 更新进度条
        function updateProgress(progress) {
            const percent = Math.round(progress * 100);
            uploadProgress.style.width = percent + '%';
            progressText.innerText = percent + '%';
        }
    </script>
</body>
</html>

后端代码
引入依赖,这里我用的阿里云的OSS的客户端(虽然back桶是在腾讯云上,但是好像也可以用。为什么没用腾讯云,是因为之前版本腾讯云的依赖更新了,报错appid懒得去看解决,就暂时用的阿里云的)。
业务主体代码

 @PostMapping("/uploadVideo")
    @ResponseBody
    public Result<String> uploadVideoChunk(@RequestPart MultipartFile videoFile, // 视频文件
                                           @RequestParam("chunkIndex") int chunkIndex, // 分片索引
                                           @RequestParam("totalChunks") int totalChunks, // 分片总数
                                           @RequestParam("fileName") String fileName, // 文件名
                                           @RequestParam("md5") String md5, // 文件MD5
                                           HttpServletRequest request) {

        try {
            // Step 1: 计算接收到的分片的 MD5,并与前端提供的 MD5 进行比较
            try (InputStream inputStream = videoFile.getInputStream()) {
                String calculatedMd5 = DigestUtils.md5Hex(inputStream);

                // 比较前端发送的 MD5 与后端计算的 MD5
                if (!calculatedMd5.equals(md5)) {
                    return Result.error("MD5 校验失败,分片数据不一致");
                }
            }

            // Step 2: 获取OSS配置,使用缓存并处理多线程安全
            Set set = cacheUtil.get(CacheNameConst.OSS, "OSS", Set.class);
            if (set == null) {
                synchronized (this) {  // 加锁
                    // 从数据库中获取配置信息并放入缓存
                    set = setService.getOne(new LambdaQueryWrapper<Set>().eq(Set::getKey, "OSS"));
                    if (set == null) {
                        cacheUtil.put(CacheNameConst.OSS, "OSS", "数据库没有数据");
                    } else {
                        // 将配置信息放入缓存
                        cacheUtil.put(CacheNameConst.OSS, "OSS", set);
                    }

                }
            }

            if (set == null) {
                return Result.error("未配置OSS");
            }

            OssSetDTO ossSetDTO = JSONUtil.toBean(set.getValues(), OssSetDTO.class);
            TXOssSetDTO dto = ossSetDTO.getTxOssSetDTO();

            // Step 3: 定义临时存储目录,用于保存分片文件
            String tempDirPath = System.getProperty("java.io.tmpdir") + "/upload_chunks/";
            File tempDir = new File(tempDirPath);
            if (!tempDir.exists()) {
                tempDir.mkdirs();  // 创建临时存储目录
            }

            // Step 4: 临时存储每个分片的文件
            String chunkFileName = tempDirPath + fileName + ".part" + chunkIndex;
            File chunkFile = new File(chunkFileName);
            videoFile.transferTo(chunkFile);  // 保存分片文件
            // 将文件名字转换成二进制
            String fileNameBytes = convertStringToBytes(fileName);
            // Step 5: 检查是否所有分片都已上传完毕
            if (allChunksUploaded(tempDirPath, fileName, totalChunks)) {
                // 合并分片
                String mergedFilePath = tempDirPath + fileName;
                // 合并分片
                mergeChunks(tempDirPath, fileName, totalChunks, mergedFilePath);
                String name = fileNameBytes + ".mp4";

                // 上传合并后的文件到OSS
                String objectName = "videos/" + name;
                OSS ossClient = new OSSClientBuilder().build(dto.getEndpoint(), dto.getSecretId(), dto.getSecretKey());

                try (FileInputStream inputStream = new FileInputStream(new File(mergedFilePath))) {
                    ossClient.putObject(dto.getBucketName(), objectName, inputStream);
                    log.info("视频上传成功");

                    // 清理临时文件
                    deleteTempFiles(tempDirPath, fileName, totalChunks);
                    deleteTempFiles(tempDirPath, fileName + ".mp4", totalChunks);

                    return Result.data(dto.getPrefix() + "/" + objectName);  // 返回视频URL
                } catch (OSSException oe) {
                    log.error("OSSException: " + oe.getErrorMessage());
                    return Result.error(201, "请检查OSS配置");
                } catch (ClientException ce) {
                    log.error("ClientException: " + ce.getMessage());
                    return Result.error(201, "请检查OSS配置");
                }
            }
            return Result.data("分片" + chunkIndex + "上传成功");

        } catch (IOException e) {
            log.error("Error calculating MD5", e);
            return Result.error("MD5 计算错误,分片上传失败");
        } catch (Exception e) {
            log.error("上传视频分片时发生错误: " + e.getMessage(), e);
            return Result.error("视频分片上传失败");
        }
    }


再来看视频文件代码

这是本地缓存的

其他代码


    // 检查是否所有分片都已上传完毕
    private boolean allChunksUploaded(String tempDirPath, String fileName, int totalChunks) {
        for (int i = 0; i < totalChunks; i++) {
            File partFile = new File(tempDirPath + fileName + ".part" + i);
            if (!partFile.exists()) {
                return false;
            }
        }
        return true;
    }

    // 合并分片
    private void mergeChunks(String tempDirPath, String fileName, int totalChunks, String mergedFilePath) throws
            IOException {
        try (BufferedOutputStream mergedStream = new BufferedOutputStream(new FileOutputStream(mergedFilePath, true))) {
            for (int i = 0; i < totalChunks; i++) {
                File partFile = new File(tempDirPath + fileName + ".part" + i);
                try (BufferedInputStream partStream = new BufferedInputStream(new FileInputStream(partFile))) {
                    byte[] buffer = new byte[32768]; // 大文件时这个就可以设置大些 一般是1024
                    int bytesRead;
                    while ((bytesRead = partStream.read(buffer)) != -1) {
                        // 将分片内容写入合并文件
                        mergedStream.write(buffer, 0, bytesRead);
                    }
                }
            }
        }
    }

    // 删除临时文件
    private void deleteTempFiles(String tempDirPath, String fileName, int totalChunks) {
        for (int i = 0; i < totalChunks; i++) {
            File partFile = new File(tempDirPath + fileName + ".part" + i);
            if (partFile.exists()) {
                partFile.delete();
            }
        }
        File mergedFile = new File(tempDirPath + fileName);
        if (mergedFile.exists()) {
            mergedFile.delete();
        }
    }

  
    // 将名字转换成二进制数组
    // 这里是为了解决上传到COS上面,判断是新增视频还是将原来的视频覆盖掉做更新处理
    public String convertStringToBytes(String fileName) {
        try {
            // 使用 UTF-8 字符编码将字符串转换为字节数组
            return fileName.getBytes("UTF-8").toString();
        } catch (UnsupportedEncodingException e) {
            // 如果字符编码不支持,抛出异常
            e.printStackTrace();
            return null;
        }
    }

我们再调用这个接口,就可以使用分片上传这个功能了

看打印日志,ok,接口调用成功了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值