整体的一个实现流程
- 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
- 使用文件的MD5值或其他标识符在数据库中查询,以检查该文件是否已存在
- 初始化一个分片上传任务,返回本次分片上传唯一标识;
- 按照一定的策略(串行或并行)发送各个分片数据块;
- 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。
流程图
基于阿里云OSS对象存储
1.首先我们通过在阿里云OSS对象存储控制台创建Bucket
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>