springboot 对接华为云obs云存储

官网文档:华为云分片上传

https://support.huaweicloud.com/sdk-java-devg-obs/obs_21_0614.html

注意:前台使用的是vue-simple-uploader组件
所以使用的对象,都是通过前台传递过来的参数,进行获取的,后台接受到参数后,将对应信息保存,主要是依赖上传分片接口传递到obs返回对应的地址

1. 笔记:

text 复制代码
分段上传分为如下3个步骤:

1.初始化分段上传任务(ObsClient.initiateMultipartUpload)。
2.逐个或并行上传段(ObsClient.uploadPart)。
3.合并段(ObsClient.completeMultipartUpload)或取消分段上传任务(ObsClient.abortMultipartUpload)。

一、初始化分段上传任务:
1.简介:
使用分段上传方式传输数据前,必须先通知OBS初始化一个分段上传任务。该操作会返回一个OBS服务端创建的全局唯一标识(Upload ID),用于标识本次分段上传任务。
您可以根据这个唯一标识来发起相关的操作,如取消分段上传任务、列举分段上传任务、列举已上传的段等。
2.方法定义:
obsClient.initiateMultipartUpload(InitiateMultipartUploadRequest request)

二、上传段(Java SDK)
1.简介:
初始化分段上传任务后,通过分段上传任务的ID,上传段到指定桶中。除了最后一段以外,其他段的大小范围是100KB~5GB;最后一段的大小范围是0~5GB。
上传的段的编号也有范围限制,其范围是1~10000。
上传段时,除了指定上传ID,还必须指定段编号。您可以选择1和10000之间的任意段编号。段编号在您正在上传的对象中唯一地标示了段及其位置。
如果您使用之前上传的段的同一段编号上传新段,则之前上传的段将被覆盖。无论您何时上传段,OBS都将在其响应中返回ETag标头。
对于每个段上传任务,(您必须记录每个段编号和ETag值)。您在后续的合并请求中需要添加这些值以完成多段上传。

2.方法定义:
obsClient.uploadPart(UploadPartRequest request)

2.1 请求中包含的参数
参数名称  参数类型   			是否必选    简述
bucketName  String   			必选		桶名
	
objectKey   String   			必选       对象名。对象名是对象在存储桶中的唯一标识。对象名是对象在桶中的完整路径,路径中不包含桶名。

partNumber  int      			必选		段号。	

uploadId	String   			必选 		分段上传任务的ID。任务ID可以通过初始化分段上传任务生成。例如:000001648453845DBB78F2340DD460D8。	

input      java.io.InputStream  可选  		待上传对象的数据流。   
											约束限制:file为null,需要设置input字段不为空;input为null ,需要设置file字段不为空。二者需要选其一。

file	   java.io.File 		可选		待上传对象的文件。

offset	   long					可选		源文件中某一分段的起始偏移大小。
											取值范围:非负整数,不大于待上传对象的大小,单位:字节。
											默认取值:0

partSize   Long					可选		当前段的长度。	
											约束限制:上传段接口要求除最后一段以外,其他的段大小都要大于100KB。
													  但是上传段接口并不会立即校验上传段的大小(因为不知道是否为最后一段),
													  只有调用合并段接口时才会校验。
										    取值范围:100KB~5GB,单位:字节。
											默认取值:102400字节


2.2 响应结果返回的参数
partNumber  段号
etag  String    
ETag是段内容的唯一标识,可以通过该值识别段内容是否有变化。
OBS会将服务端收到段数据的ETag值(段数据的MD5值)返回给用户。
比如上传段时ETag为A,下载段时ETag为B,则说明段内容发生了变化。
ETag只反映变化的内容,而不是其元数据。上传的段或拷贝操作创建的段,都有唯一的ETag。


三、 合并段
1.简介:
如果用户上传完所有的段,就可以调用合并段接口,系统将在服务端将用户指定的段合并成一个完整的对象。
在执行"合并段"操作以前,用户不能下载已经上传的数据。
在合并段时需要将多段上传任务初始化时记录的附加消息头信息拷贝到对象元数据中,其处理过程和普通上传对象带这些消息头的处理过程相同。
在并发合并段的情况下,仍然遵循Last Write Win策略,但"Last Write"的时间定义为段任务的初始化时间。


已经上传的段,只要没有取消对应的多段上传任务,都要占用用户的容量配额;
对应的多段上传任务"合并段"操作完成后,只有指定的多段数据占用容量配额,
用户上传的其他此多段任务对应的段数据如果没有包含在"合并段"操作制定的段列表中,
"合并段"完成后系统将删除多余的段数据,且同时释放容量配额。


合并段时,OBS通过按升序的段编号规范化多段来创建对象。
如果在初始化上传段任务中提供了任何对象元数据,则OBS会将该元数据与对象相关联。成功完成请求后,段将不再存在。
合并段请求必须包括(上传ID、段编号和相应的ETag值的列表)。
OBS响应包括可唯一地识别组合对象数据的ETag。此ETag无需成为对象数据的MD5哈希。


2.方法定义
obsClient.completeMultipartUpload(CompleteMultipartUploadRequest request)

2.1 请求中包含的参数
参数名称  参数类型   		是否必选    简述
bucketName  String          必选		桶名
	
objectKey   String          必选        对象名。对象名是对象在存储桶中的唯一标识。对象名是对象在桶中的完整路径,路径中不包含桶名。

partEtag    List<PartEtag>  必选        待合并的段列表。

uploadId    String          必选        分段上传任务的ID,例如:000001648453845DBB78F2340DD460D8 长度为32的字符串。


2.2 PartEtag 包含参数
etag       String         必选   	段的ETag值。分段的Base64编码的128位MD5摘要。

partNumber  Integer		  必选 	    段号。分段号可以是不连续的。   取值范围是[1,10000]的非负整数


2.3 CompleteMultipartUploadResult 返回结果说明:     

参数名称		   参数类型		  描述

statusCode    	   int			 HTTP状态码。

responseHeaders  Map<String, Object>  响应消息头列表,由多个元组构成。元组中String代表响应消息头的名称,Object代表响应消息头的值。

etag			String		
 
bucketName      String       合并段所在的桶的桶名。
 
objectKey		String		 合并段后得到的对象名。对象名是对象在存储桶中的唯一标识。对象名是对象在桶中的完整路径,路径中不包含桶名。
							 例如,您对象的访问地址为examplebucket.obs.cn-north-4.myhuaweicloud.com/folder/test.txt 中,对象名为folder/test.txt。

location        String  	 合并段后得到的对象的url。  例如:https://example-Bucket.obs.regions.myhuaweicloud.com/example-Object


versionId       String  	 合并段后得到的对象版本号。如果桶的多版本状态为开启,则会返回对象的版本号。


objectUrl      	String		 合并段后得到的对象的全路径。


encodingType    String		 用于指定对响应中的对象名objectKey进行指定类型的编码。如果objectKey包含xml 1.0标准不支持的控制字符,可通过设置该参数对响应中的objectKey进行编码。

2. 展示对应代码

2.1 表结构

sql 复制代码
-- pfqv_v2_test_prod0603.file_upload_log definition

CREATE TABLE `file_upload_log` (
  `id` bigint NOT NULL COMMENT '主键ID',
  `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件名',
  `identifier` char(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件标识, MD5(文件加密后的名称)',
  `object_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '对象名。对象名是对象在存储桶中的唯一标识。对象名是对象在桶中的完整路径,路径中不包含桶名。',
  `chunk_size` bigint DEFAULT NULL COMMENT '分片大小',
  `file_chunk_num` int DEFAULT NULL COMMENT '文件分片总数',
  `upload_chunk_number` int DEFAULT NULL COMMENT '已上传的分片数量',
  `upload_chunk_index` int DEFAULT NULL COMMENT '上次上传的分片位置',
  `total_size` bigint DEFAULT NULL COMMENT '文件分片总大小',
  `upload_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT 'OBS返回的uploadId',
  `part_etags_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT 'partEtags对象,合并文件时所需列表',
  `file_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件上传成功后的链接',
  `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '0' COMMENT '删除标志(0代表存在 2代表删除)',
  `create_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '创建者',
  `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_by` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '' COMMENT '更新者',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `idx_identifier` (`identifier`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC COMMENT='记录文件上传进度记录';

2.2 分片上传代码

2.2.1 查看状态接口

Controller 控制层

java 复制代码
    /**
     * 检查上传进度
     */
    @GetMapping("/uploadVideo")
    public AjaxResult check(@ModelAttribute FileUploadRequest req) {
        CheckResponse checkVo =  fsUploadFileService.checkUploadStatus(req);
        return AjaxResult.success(checkVo);
    }

Service

java 复制代码
 CheckResponse checkUploadStatus(FileUploadRequest req);

ServiceImpl

java 复制代码
 /**
     * 该方法用于检查文件上传的状态,并返回相应的结果。
     * 如果文件未上传,则初始化上传并记录相关信息;
     * 如果文件已经部分上传,则返回已上传的分片信息,方便继续上传未完成的部分。
     * @param req
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public CheckResponse checkUploadStatus(FileUploadRequest req) {
        // 压缩文件
        // 文件的唯一标识符,通常是文件内容的 MD5 值
        String md5 = req.getIdentifier();
        // 文件总共分为多少个分片
        Integer chunkTotal = req.getTotalChunks();
        // 每个分片的大小
        Long chunkSize = req.getChunkSize();
        // 文件的总大小
        Long totalSize = req.getTotalSize();
        // 当前分片的位置 - 当前分片的序号
//        Integer chunkNumber = req.getChunkNumber();

        CheckResponse checkVo = new CheckResponse();
        Long userId = SecurityUtils.getUserId();

        // 2.获取 bucketName 和 objectKey
        // 获取文件的扩展名
        String filename = req.getFilename();
        String extension =filename.substring(filename.lastIndexOf('.') + 1);
        // 目标存储的文件名,由 md5 和文件扩展名组成
        String objectKey = md5 + "." + extension;
        String folderName = "";
        if (StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)){
            folderName = "5S/images/";
        }else{
            folderName = "5S/video/";
        }
        objectKey = folderName + objectKey;
        // 从配置中获取的 OBS 存储桶名称


//        //拼接上传路径
//        String separator = "/";
//        String objectKey = "creation" + separator + agencyCode +separator+competitionId + separator+userId+separator+fileName;

        FileUploadLog fileUploadLog = fileUploadLogMapper.selectFileUploadLogByFileMd5(md5);
        List<Integer> chunkIndexes = new ArrayList<>();
        // 上传文件不存在
        if(fileUploadLog == null){
            try {
                //代表不存在(未上传)
                checkVo.setUploaded(false);
                // 表示没有上传的分片
                checkVo.setChunkList(chunkIndexes);

                //插入一条
                InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectKey);
                InitiateMultipartUploadResult result = obsClient.initiateMultipartUpload(request);
                final String uploadId = result.getUploadId();
                checkVo.setUploadId(uploadId);
                FileUploadLog fileUploadLogInsert = new FileUploadLog();
                fileUploadLogInsert.setId(IdUtils.longUUID());
                fileUploadLogInsert.setUploadId(uploadId);
                fileUploadLogInsert.setIdentifier(md5);
                fileUploadLogInsert.setChunkSize(chunkSize);
                fileUploadLogInsert.setObjectKey(objectKey);
                fileUploadLogInsert.setFileChunkNum(chunkTotal);
//            fileUploadLogInsert.setUploadChunkIndex(chunkNumber);
                fileUploadLogInsert.setCreateTime(DateUtils.getNowDate());
                fileUploadLogInsert.setCreateBy(userId.toString());
                fileUploadLogInsert.setTotalSize(totalSize);
                fileUploadLogMapper.insertFileUploadLog(fileUploadLogInsert);
            } catch (ObsException e) {
                log.error("OBS 上传分片失败", e);
                // 打印详细的OBS错误信息
                System.out.println("HTTP Code:" + e.getResponseCode());
                System.out.println("Error Code:" + e.getErrorCode());
                System.out.println("Error Message:" + e.getErrorMessage());
                System.out.println("Request ID:" + e.getErrorRequestId());
                System.out.println("Host ID:" + e.getErrorHostId());
                e.printStackTrace();
            }
        }
        else {
            // 文件已部分或完全上传的情况
            // 已上传的最后一个分片序号
            Integer uploadChunkIndex = fileUploadLog.getUploadChunkIndex();
            checkVo.setUploaded(false);
            for (int i= 1; i <= uploadChunkIndex; i++)  {
                // 将 uploadChunkIndex 之前的所有分片编号添加到 chunkIndexes 列表中
                chunkIndexes.add(i);
            }
            checkVo.setChunkList(chunkIndexes);
            // 表示所有分片都已上传完成
            if(uploadChunkIndex.longValue() == chunkTotal){
                checkVo.setUploaded(true);
                checkVo.setFileName(fileUploadLog.getFileUrl());
            }

        }
        return checkVo;
    }

分片上传接口

Controller

java 复制代码
    /**
     * 上传分片
     */
    @PostMapping("/uploadVideo")
    @ApiOperation(value = "断点续传-上传分片")
    public AjaxResult uploadChunk(@ModelAttribute  FileUploadRequest req) throws IOException {
        return fsUploadFileService.uploadChunk(req);
    }

Service

java 复制代码
  AjaxResult uploadChunk(FileUploadRequest req);

ServiceImpl

java 复制代码
    @Transactional(rollbackFor = Exception.class)
    @Override
    public  AjaxResult uploadChunk(FileUploadRequest req) {
    try {
        String md5 = req.getIdentifier();  // 当前上传文件的 MD5 值
        MultipartFile file = req.getFile(); //当前分片二进制
        Integer index = req.getChunkNumber();  // 当前分片的位置
        Integer chunkTotal = req.getTotalChunks();  // 分片总数
        Long totalSize = req.getTotalSize();
        Long currentChunkSize = req.getCurrentChunkSize();
        String uploadId = req.getUploadId();
        // 获取 bucketName 和 objectKey
        Long userId = SecurityUtils.getUserId();

        FileUploadLog fileUploadLog = fileUploadLogMapper.selectFileUploadLogByFileMd5(md5);
        if (fileUploadLog == null) {
            return AjaxResult.error("上传记录不存在");
        }

        Long chunkSize = fileUploadLog.getChunkSize();
        String objectKey = fileUploadLog.getObjectKey();
        // 上传段
        UploadPartRequest uploadPartRequest = new UploadPartRequest();
            Long offset = (index - 1) * chunkSize;
            uploadPartRequest.setBucketName(bucketName);
            uploadPartRequest.setObjectKey(objectKey);
            uploadPartRequest.setUploadId(uploadId);
            uploadPartRequest.setPartNumber(index);
            uploadPartRequest.setInput(file.getInputStream());
            uploadPartRequest.setPartSize(currentChunkSize);
            uploadPartRequest.setOffset(offset);
            obsClient.uploadPart(uploadPartRequest);

            // 列举已上传的段,其中uploadId来自于initiateMultipartUpload
             ListPartsRequest request = new ListPartsRequest(bucketName, objectKey);
             request.setUploadId(uploadId);
             ListPartsResult result = obsClient.listParts(request);
             List<Multipart> multipartList = result.getMultipartList();
             LinkedList<PartEtag> partEtags = new LinkedList();
            if (multipartList != null && multipartList.size() > 0){
                     multipartList.stream().forEach(multipart -> {
                         PartEtag partEtag = new PartEtag();
                         partEtag.setEtag(multipart.getEtag());
                         partEtag.setPartNumber(multipart.getPartNumber());
                         partEtags.add(partEtag);
                     });
             }
            FileUploadLog fileUploadLogUpdate = new FileUploadLog();
            if (partEtags.size() == chunkTotal.intValue()){
                fileUploadLogUpdate.setId(fileUploadLog.getId());
                fileUploadLogUpdate.setFileName(file.getName());
//                fileUploadLogUpdate.setUploadChunkNumber((fileUploadLog.getUploadChunkNumber() != null ? fileUploadLog.getUploadChunkNumber() : 0) + 1);
//                fileUploadLogUpdate.setUploadChunkIndex(index);
                fileUploadLogUpdate.setPartEtagsJson(partEtags.toString());
                fileUploadLogUpdate.setUpdateTime(DateUtils.getNowDate());
                fileUploadLogUpdate.setUpdateBy(userId.toString());

                CompleteMultipartUploadRequest completeRequest =
                        new CompleteMultipartUploadRequest(bucketName, objectKey, uploadId, partEtags);
                CompleteMultipartUploadResult completeResult = obsClient.completeMultipartUpload(completeRequest);
                String objectUrl = completeResult.getObjectUrl();
                objectUrl = objectUrl.replace("ringpai-oa.obs.cn-north-4.myhuaweicloud.com:443", "oas.ringpai.com");
                fileUploadLogUpdate.setFileUrl(objectUrl);
                // 在所有操作成功后才更新数据库
                fileUploadLogMapper.updateFileUploadLog(fileUploadLogUpdate);
                return AjaxResult.success("文件上传成功", fileUploadLogUpdate.getFileUrl());
            }else{
                Map<String,Object> map = new HashMap();
                map.put("index",index);
                map.put("partEtags",partEtags);
                return AjaxResult.success("文件分片上传成功", map);
            }
        } catch (ObsException e) {
            log.error("OBS 上传分片失败", e);
            // 打印详细的OBS错误信息
            System.out.println("HTTP Code:" + e.getResponseCode());
            System.out.println("Error Code:" + e.getErrorCode());
            System.out.println("Error Message:" + e.getErrorMessage());
            System.out.println("Request ID:" + e.getErrorRequestId());
            System.out.println("Host ID:" + e.getErrorHostId());
            // 抛出运行时异常以回滚事务,防止数据库被更新
            throw new RuntimeException("OBS 上传分片失败", e);
        } catch (Exception e) {
            log.error("上传分片失败", e);
            // 抛出运行时异常以回滚事务,防止数据库被更新
            throw new RuntimeException("上传分片失败", e);
        }finally {
      }
    }

3. 使用到的前台对象

FileUploadRequest

java 复制代码
package com.ruoyi.codecleanliness5s.domain.vo;

import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
/**
 * 每一个上传块都会包含如下分块信息:
 * chunkNumber: 当前块的次序,第一个块是 1,注意不是从 0 开始的。
 * totalChunks: 文件被分成块的总数。
 * chunkSize: 分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大。
 * currentChunkSize: 当前块的大小,实际大小。
 * totalSize: 文件总大小。
 * identifier: 这个就是每个文件的唯一标示,md5码
 * filename: 文件名。
 * relativePath: 文件夹上传的时候文件的相对路径属性。
 * 一个分块可以被上传多次,当然这肯定不是标准行为,,这种重传也但是在实际上传过程中是可能发生这种事情的是本库的特性之一。
 * <p>
 * 根据响应码认为成功或失败的:
 * 200 文件上传完成
 * 201 文加快上传成功
 * 500 第一块上传失败,取消整个文件上传
 * 507 服务器出错自动重试该文件块上传
 */
@Data
public class FileUploadRequest {
    /**
     * 主键ID
     */
    private Long id;
    /**
     * 当前文件块,从1开始
     */
    private Integer chunkNumber;
    /**
     * 分块大小
     */
    private Long chunkSize;
    /**
     * 当前分块大小
     */
    private Long currentChunkSize;
    /**
     * 总大小
     */
    private Long totalSize;
    /**
     * 文件标识
     */
    private String identifier;
    /**
     * 文件名
     */
    private String filename;
    /**
     * 相对路径
     */
    private String relativePath;
    /**
     * 总块数
     */
    private Integer totalChunks;

    /**
     * 二进制文件
     */
    private MultipartFile file;

    private String uploadId;
}

CheckResponse

java 复制代码
package com.ruoyi.codecleanliness5s.domain.vo;

import lombok.Data;

import java.util.List;

@Data
public class CheckResponse {
    // 上传状态
    private boolean uploaded;
    // 分片列表
    private List<Integer> chunkList;
    private String fileName;
    private String uploadId;
}
相关推荐
郑祎亦38 分钟前
Spring Boot 项目 myblog 整理
spring boot·后端·java-ee·maven·mybatis
本当迷ya1 小时前
💖2025年不会Stream流被同事排挤了┭┮﹏┭┮(强烈建议实操)
后端·程序员
计算机毕设指导62 小时前
基于 SpringBoot 的作业管理系统【附源码】
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
学习向前冲2 小时前
CCE-基础
华为云
paopaokaka_luck2 小时前
[371]基于springboot的高校实习管理系统
java·spring boot·后端
捂月3 小时前
Spring Boot 深度解析:快速构建高效、现代化的 Web 应用程序
前端·spring boot·后端
瓜牛_gn4 小时前
依赖注入注解
java·后端·spring
Estar.Lee4 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
喜欢猪猪4 小时前
Django:从入门到精通
后端·python·django
一个小坑货4 小时前
Cargo Rust 的包管理器
开发语言·后端·rust