阿里云 OSS大文件分片上传

整体的一个实现流程

  1. 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
  2. 使用文件的MD5值或其他标识符在数据库中查询,以检查该文件是否已存在
  3. 初始化一个分片上传任务,返回本次分片上传唯一标识;
  4. 按照一定的策略(串行或并行)发送各个分片数据块;
  5. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

流程图

基于阿里云OSS对象存储

1.首先我们通过在阿里云OSS对象存储控制台创建Bucket

进入阿里云登录页 (aliyun.com)

2.进入OSS对象存储控制台

点击创建Bucket

设置Bucket属性

首先创建RAM用户

创建成功以后 我们需要到ARM去开启授权

到此前提工作已做好

接下来上代码

首先在yml配置这些 

AccessKeyId需要自己创建

文件表



SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_file
-- ----------------------------
DROP TABLE IF EXISTS `t_file`;
CREATE TABLE `t_file`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '文件id',
  `file_path` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件存储路径',
  `file_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件名称',
  `file_type` varchar(24) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件类型',
  `file_suffix` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件后缀',
  `file_size` double NULL DEFAULT NULL COMMENT '文件大小',
  `file_md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件MD5:校验文件是否已经存在',
  `is_sharding` int(11) NULL DEFAULT NULL COMMENT '是否分片 0:未分片 1:以分片',
  `sharding_num` int(11) NULL DEFAULT NULL COMMENT '分片数量',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  `create_by` int(11) NULL DEFAULT NULL,
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `update_by` int(11) NULL DEFAULT NULL,
  `is_delete` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 37 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '上传文件表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of t_file
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

分片表


SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_file_sharding
-- ----------------------------
DROP TABLE IF EXISTS `t_file_sharding`;
CREATE TABLE `t_file_sharding`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '分片id',
  `file_md5` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '文件md5',
  `serial_number` int(11) NULL DEFAULT NULL COMMENT '分片序号',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  `create_by` int(11) NULL DEFAULT NULL,
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `update_by` int(11) NULL DEFAULT NULL,
  `is_delete` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 78 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '分片表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Records of t_file_sharding
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

后端 Controller

/**
 * @descibe oss
 * @date 2020/5/27 13:19
 */
@RestController
@Log4j2
@Api(tags = "分片上传")
public class UpLoadController {
    @Autowired
    private UpLoadService upLoadService;
    /**
     * 分片上传
     * @param uploadParams
     * @return  Result
     */
    @ApiOperation(value = "分片上传")
    @PostMapping("/file/uploadFile")
    public Result<Map<String,Object>> fileUploadZone(UploadParams uploadParams) {
        return upLoadService.fileUploadZone(uploadParams);
    }
}

Service

package com.health.cloud.file.server.service.impl;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClient;
import com.aliyun.oss.model.*;
import com.health.cloud.common.result.Result;
import com.health.cloud.common.util.StringUtils;
import com.health.cloud.file.entity.FileEntity;
import com.health.cloud.file.entity.ShardEntity;
import com.health.cloud.file.entity.UploadParams;
import com.health.cloud.file.enums.FilePartition;
import com.health.cloud.file.exception.ServiceException;
import com.health.cloud.file.server.mapper.UpLoadMapper;
import com.health.cloud.file.server.service.UpLoadService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.InputStream;
import java.util.*;
/**
 * @ClassName UpLoadServiceImpl
 * @Description 描述
 * @Author MDY
 * @Date 2024/5/26 15:24
 */
@Service
@Slf4j
public class UpLoadServiceImpl implements UpLoadService {

    @Value("${aliyun.oss.file.endpoint}")
    private String endpoint;

    @Value("${aliyun.oss.file.accessKeyId}")
    private String accessKeyId;

    @Value("${aliyun.oss.file.accessKeySecret}")
    private String secretAccessKey;

    @Value("${aliyun.oss.file.bucketName}")
    private String bucketName;
    @Autowired
    private UpLoadMapper upLoadMapper;
    @Override
    public Result<Map<String, Object>> fileUploadZone(UploadParams uploadParams) {
            //参数校验
            checkParams(uploadParams);
            FileEntity fileEntity = new FileEntity();//创建文件对象 存储数据库
            //判断是否分片 这里如果文件大于10MB是需要分片的
            if(uploadParams.getFileSize()>10){
                //需要分片 赋值
                fileEntity.setIsSharding(FilePartition.PARTITIONED.getValue());
                fileEntity.setShardingNum(uploadParams.getChunkCount());
            }else {
                //不需要分片分片数量直接赋值1
                fileEntity.setIsSharding(FilePartition.NOT_PARTITIONED.getValue());
                fileEntity.setShardingNum(1);
            }
            //文件大小
            fileEntity.setFileSize(uploadParams.getFileSize());
            //获取文件的名称
            String originalFileName = uploadParams.getFilename();
            //文件后缀
            String suffix = originalFileName.substring(originalFileName.lastIndexOf(".") + 1);
            //重新命名文件
            String pack = "file/";
            String fileName = "file_" + System.currentTimeMillis() + "." + suffix;
            String objectName = pack + fileName;
            String url = "http://" + bucketName + "." + endpoint + "/" + objectName;
            //文件类型 通过文件 后缀 来给文件类型赋值 比如 .mp4为 视频类
            String fileType;
            switch (suffix.toLowerCase()) {
                case "mp4":
                case "avi":
                    fileType = "视频类";
                    break;
                case "jpg":
                case "jpeg":
                case "png":
                    fileType = "图片类";
                    break;
                case "doc":
                case "docx":
                case "txt":
                    fileType = "文档类";
                    break;
                case "xls":
                case "xlsx":
                    fileType = "表格类";
                    break;
                default:
                    fileType = "其他类型";
                    break;
            }
            // 设置文件类型和后缀
            fileEntity.setFileType(fileType);
            fileEntity.setFileSuffix(suffix);
            //文件名称
            fileEntity.setFileName(fileName);
            //文件路径

            //文件md5
            fileEntity.setFileMd5(uploadParams.getIdentifier());
        OSS ossClient=null;
        Date date = new Date();//创建时间对象 用来获取上传所用时长
        String beginTime = date.getHours() + ":" + date.getMinutes() + ":" + date.getSeconds();
        try {
            log.info("开始上传时间:"+beginTime);
            // 创建OSSClient实例。
            ossClient = new OSSClient(endpoint, accessKeyId, secretAccessKey);
            // 创建InitiateMultipartUploadRequest对象。
            InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, objectName);
            // 初始化分片。
            InitiateMultipartUploadResult upresult = ossClient.initiateMultipartUpload(request);
            // 返回uploadId,它是分片上传事件的唯一标识,可以根据这个uploadId发起相关的操作,如取消分片上传、查询分片上传等。
            String uploadId = upresult.getUploadId();
            // partETags是PartETag的集合。PartETag由分片的ETag和分片号组成。
            List<PartETag> partETags = new ArrayList<PartETag>();
            // 计算文件有多少个分片。
            int totalChunks =  uploadParams.getChunkCount();
            //分片的大小
            Long partSize =  uploadParams.getChunkSize();
            long fileLength = uploadParams.getFile().getSize();
            //创建文件分片关联表对象
            ShardEntity shardEntity = new ShardEntity();
            // 遍历分片上传。
            for (int i = 0; i < totalChunks; i++) {
                try {
                    long startPos = i * partSize;
                    long curPartSize = (i + 1 == totalChunks) ? (fileLength - startPos) : partSize;
                    // 跳过已经上传的分片。
                    InputStream stream = uploadParams.getFile().getInputStream();
                    stream.skip(startPos);
                    UploadPartRequest uploadPartRequest = new UploadPartRequest();
                    uploadPartRequest.setBucketName(bucketName);
                    uploadPartRequest.setKey(objectName);
                    uploadPartRequest.setUploadId(uploadId);
                    uploadPartRequest.setInputStream(stream);
                    // 设置分片大小。除了最后一个分片没有大小限制,其他的分片最小为100 KB。
                    uploadPartRequest.setPartSize(curPartSize);
                    // 设置分片号。每一个上传的分片都有一个分片号,取值范围是1~10000,如果超出这个范围,OSS将返回InvalidArgument的错误码。
                    uploadPartRequest.setPartNumber(i + 1);
                    //添加分片序号
                    shardEntity.setSerialNumber(i+1);
                    //赋值md5
                    shardEntity.setFileMd5(uploadParams.getIdentifier());
                    //添加分片关联表
                     upLoadMapper.addFileShard(shardEntity);
                    // 每个分片不需要按顺序上传,甚至可以在不同客户端上传,OSS会按照分片号排序组成完整的文件。
                    UploadPartResult uploadPartResult = ossClient.uploadPart(uploadPartRequest);
                    // 每次上传分片之后,OSS的返回结果包含PartETag。PartETag将被保存在partETags中。
                    partETags.add(uploadPartResult.getPartETag());

                } catch (Exception e) {
                    e.printStackTrace();
                    // 处理分片上传失败的情况,例如记录日志或者进行异常处理
                    // 如果需要在上传失败时返回相应信息,可以将错误信息存储起来,最后返回给用户
                    // 这里简单示例为将异常信息存储到map中,最后返回给用户
                    Map<String, Object> errorMap = new HashMap<>();
                    errorMap.put("chunkNumber", i + 1); // 存储失败的分片号
                    errorMap.put("errorMessage", e.getMessage()); // 存储失败时的异常信息

                    return Result.error(errorMap); // 返回失败信息
                }
            }
            /**
             * 创建CompleteMultipartUploadRequest对象。
             * 在执行完成分片上传操作时,需要提供所有有效的partETags。OSS收到提交的partETags后,会逐一验证每个分片的有效性。
             * 当所有的数据分片验证通过后,OSS将把这些分片组合成一个完整的文件。
             */
            CompleteMultipartUploadRequest uploadRequest = new CompleteMultipartUploadRequest(bucketName, objectName, uploadId, partETags);
            // 在完成文件上传的同时设置文件访问权限。
            uploadRequest.setObjectACL(CannedAccessControlList.PublicRead);
            // 完成上传。
            ossClient.completeMultipartUpload(uploadRequest);
            // 关闭OSSClient。
            ossClient.shutdown();
            Date date2 = new Date();
            String endTime = date2.getHours() + ":" + date2.getMinutes() + ":" + date2.getSeconds();
            log.info("结束上传时间:"+endTime+" 耗时:"+(date2.getTime()-date.getTime())/1000);
            Map<String, Object> map = new HashMap<>();
            map.put("url", url);//文件存储路径
            map.put("name", fileName);//文件名称
            map.put("suffix", suffix);//文件后缀(返回前端,如果传的不是视频类可以根据此后缀进行判断)
            //上传完毕存储数据库中
            fileEntity.setFilePath(url);
            //设置上传人 以及上传时间 //通过调用登录获取用户信息
            upLoadMapper.addFile(fileEntity);
            return Result.success(map);
        } catch (Exception e) {
            ossClient.shutdown();
            log.error(e.getMessage());
            throw  new  ServiceException("上传失败,错误信息:",500);
        }
    }

    /*参数校验*/
    private void checkParams(UploadParams uploadParams) {
        if(StringUtils.isBlank(uploadParams.getIdentifier())){
            throw new ServiceException("文件md5不能为空");
        }
        if(StringUtils.isBlank(uploadParams.getFilename())){
            throw new ServiceException("文件名不能为空");
        }
        //根据文件md5判断数据库是否已经上传此文件
        int count= upLoadMapper.isExist(uploadParams.getIdentifier());
        if(count!=0){
            throw new ServiceException("该文件已经上传,不能重复上传");
        }
    }


}

实体类

package com.health.cloud.file.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.web.multipart.MultipartFile;


@Data
@AllArgsConstructor
@NoArgsConstructor
public class UploadParams {
    private String identifier;//文件md5
    private String filename;//文件名
    private int chunkCount;//分片总数
    private Long chunkSize;//分片大小
    private MultipartFile file;//文件
    private Double fileSize;//文件大小(MB)
}

前端Vue

<template>
  <div class="upload">
    <el-upload class="upload-demo" action="#" :on-change="uploadFile" :show-file-list="true" :file-list="fileList" :auto-upload="false" ref="uploadfile" :limit="1">
      <el-button size="small" type="primary" :loading="loadingFile">上传文件</el-button>
     </el-upload>
    <el-button size="small" type="primary" @click="gotoMain('/')">返回主页</el-button>
  </div>
</template>

<script>
import SparkMD5 from "spark-md5";
import 'element-ui/lib/theme-chalk/index.css';
const chunkSize = 10 * 1024 * 1024;//定义分片的大小 暂定为10M,方便测试
import axios from "axios";
export default {
  name: 'VedioUpload',  //上传视频页面
  components: {},
  props: {},
  data() {
    return {
      fileList: [],
      loadingFile: false,
      videos :[]//存放视频的url
    }
  },
  watch: {},
  computed: {},
  methods: {
    /**
     * 上传文件
     */
    async uploadFile(File) {
      this.loadingFile = true
      console.log(File)
      //获取用户选择的文件
      const file = File.raw
      console.log(file)
      //文件大小
      const fileSize = File.size
      // 放入文件列表
      this.fileList = [{ "name": File.name }]
      // 可以设置大于多少兆可以分片上传,否则走普通上传
      //计算当前选择文件需要的分片数量
      const chunkCount = Math.ceil(fileSize / chunkSize)

      console.log("文件大小:", (File.size / 1024 / 1024) + "Mb", "分片数:", chunkCount)
      //获取文件md5
      const fileMd5 = await this.getFileMd5(file, chunkCount);

      console.log("文件md5:", fileMd5)
      console.log("向后端请求本次分片上传初始化")

      const formData = new FormData();
      formData.append('file', file); // 添加文件内容
      formData.append('filename', File.name); // 添加文件名
      formData.append('identifier', fileMd5); // 添加文件MD5值
      formData.append('chunkCount', chunkCount); // 添加分片数量
      formData.append('chunkSize', chunkSize); // 添加单个分片大小
      formData.append('fileSize', fileSize / 1024 / 1024); // 添加文件大小(MB)

      // 在你的代码中使用Axios发送请求
      axios.post('/api/file/file/uploadFile', formData)
          .then(response => {
            const res = response.data.data;
            console.log(res)
            this.$router.replace({path:'/VideoPage'})
          })
    },

    /**
     * 获取文件MD5
     * @param file
     * @returns {Promise<unknown>}
     */
    getFileMd5(file, chunkCount) {
      return new Promise((resolve, reject) => {
        let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
        let chunks = chunkCount;
        let currentChunk = 0;
        let spark = new SparkMD5.ArrayBuffer();
        let fileReader = new FileReader();

        fileReader.onload = function (e) {
          spark.append(e.target.result);
          currentChunk++;
          if (currentChunk < chunks) {
            loadNext();
          } else {
            let md5 = spark.end();
            resolve(md5);
          }
        };
        fileReader.onerror = function (e) {
          reject(e);
        };
        function loadNext() {
          let start = currentChunk * chunkSize;
          let end = start + chunkSize;
          if (end > file.size) {
            end = file.size;
          }
          fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
        }
        loadNext();
      });


    },
    gotoMain(path){
      this.$router.replace({path:path})
    }
  },
  created() { },
  mounted() { }
}
</script>
<style lang="sass" scoped>
</style>

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值