在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,当文件上传到一半中断后,继续上传却只能重头开始上传,这种让人不爽的体验。
1、秒传
通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了。
前端:
// 生成文件标识,标识多次上传的是不是同一个文件
let key = hex_md5(file.name + file.size + file.type);
let key10 = parseInt(key, 16);
let key62 = Tool._10to62(key10);
// 文件分片
// let shardSize = 10 * 1024 * 1024; //以10MB为一个分片
// let shardSize = 50 * 1024; //以50KB为一个分片
let shardSize = _this.shardSize;
let shardIndex = 1; //分片索引,1表示第1个分片
let size = file.size;
let shardTotal = Math.ceil(size / shardSize); //总片数
let param = {
'shardIndex': shardIndex,
'shardSize': shardSize,
'shardTotal': shardTotal,
'use': _this.use,
'name': file.name,
'suffix': suffix,
'size': file.size,
'key': key62
};
_this.check(param);
check (param) {
let _this = this;
_this.$ajax.get(process.env.VUE_APP_SERVER + '/file/admin/check/' + param.key).then((response)=>{
let resp = response.data;
if (resp.success) {
let obj = resp.content;
if (!obj) {
param.shardIndex = 1;
console.log("没有找到文件记录,从分片1开始上传");
_this.upload(param);
} else if (obj.shardIndex === obj.shardTotal) {
// 已上传分片 = 分片总数,说明已全部上传完,不需要再上传
Toast.success("文件极速秒传成功!");
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
}
后端:
@GetMapping("/check/{key}")
public ResponseDto check(@PathVariable String key) throws Exception {
LOG.info("检查上传分片开始:{}", key);
ResponseDto responseDto = new ResponseDto();
FileDto fileDto = fileService.findByKey(key);
if (fileDto != null) {
if (StringUtils.isEmpty(fileDto.getVod())) {
fileDto.setPath(OSS_DOMAIN + fileDto.getPath());
} else {
DefaultAcsClient vodClient = VodUtil.initVodClient(accessKeyId, accessKeySecret);
GetMezzanineInfoResponse response = VodUtil.getMezzanineInfo(vodClient, fileDto.getVod());
System.out.println("获取视频信息, response : " + JSON.toJSONString(response));
String fileUrl = response.getMezzanine().getFileURL();
fileDto.setPath(fileUrl);
}
}
responseDto.setContent(fileDto);
return responseDto;
}
数据库:
当上传文件的时候,前端将(文件名字、文件大小、文件类型)用md5加密,生成文件标识,标识多次上传的是不是同一个文件。
从数据库字段中可以看出key字段就是用来表示文件的,size是文件总大小,shard_index是已上传分片,shard_size是分片大小,shard_total分片总数。
秒传的逻辑是选择一个文件,进行md5加密,然后后端查找是否存在相同的key,如果存在,再判断shard_index和shard_total是否相等,如果相等,则前端直接显示秒传成功。
2、分片上传
(1).大文件上传
(2).网络环境环境不好,存在需要重传风险的场景
断点续传
断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传。
实现步骤:
当数据库没有找到key,或者找到key了还没上传完全,就会执行分片上传
这里采用oss的sdk进行分片上传。
前端:
/**
* 将分片数据转成base64进行上传
*/
upload (param) {
let _this = this;
let shardIndex = param.shardIndex;
let shardTotal = param.shardTotal;
let shardSize = param.shardSize;
let fileShard = _this.getFileShard(shardIndex, shardSize);
// 将图片转为base64进行传输
let fileReader = new FileReader();
Progress.show(parseInt((shardIndex - 1) * 100 / shardTotal));
fileReader.onload = function (e) {
let base64 = e.target.result;
// console.log("base64:", base64);
param.shard = base64;
_this.$ajax.post(process.env.VUE_APP_SERVER + '/file/admin/' + _this.url, param).then((response) => {
let resp = response.data;
console.log("上传文件成功:", resp);
Progress.show(parseInt(shardIndex * 100 / shardTotal));
if (shardIndex < shardTotal) {
// 上传下一个分片
param.shardIndex = param.shardIndex + 1;
_this.upload(param);
} else {
Progress.hide();
_this.afterUpload(resp);
$("#" + _this.inputId + "-input").val("");
}
});
};
fileReader.readAsDataURL(fileShard);
},
getFileShard (shardIndex, shardSize) {
let _this = this;
let file = _this.$refs.file.files[0];
let start = (shardIndex - 1) * shardSize; //当前分片起始位置
let end = Math.min(file.size, start + shardSize); //当前分片结束位置
let fileShard = file.slice(start, end); //从文件中截取当前的分片数据
return fileShard;
},
后端:
@PostMapping("/oss-append")
public ResponseDto fileUpload(@RequestBody FileDto fileDto) throws Exception {
LOG.info("上传文件开始");
String use = fileDto.getUse();
String key = fileDto.getKey();
String suffix = fileDto.getSuffix();
Integer shardIndex = fileDto.getShardIndex();
Integer shardSize = fileDto.getShardSize();
String shardBase64 = fileDto.getShard();
MultipartFile shard = Base64ToMultipartFile.base64ToMultipart(shardBase64);
// 保存文件到本地
FileUseEnum useEnum = FileUseEnum.getByCode(use);
// //如果文件夹不存在则创建
String dir = useEnum.name().toLowerCase();
String path = new StringBuffer(dir)
.append("/")
.append(key)
.append(".")
.append(suffix)
.toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
ObjectMetadata meta = new ObjectMetadata();
// 指定上传的内容类型。
meta.setContentType("text/plain");
// 通过AppendObjectRequest设置多个参数。
AppendObjectRequest appendObjectRequest = new AppendObjectRequest(bucket, path, new ByteArrayInputStream(shard.getBytes()),meta);
// 第一次追加。
// 设置文件的追加位置。
// appendObjectRequest.setPosition(0L);
appendObjectRequest.setPosition((long) ((shardIndex - 1) * shardSize));
AppendObjectResult appendObjectResult = ossClient.appendObject(appendObjectRequest);
// 文件的64位CRC值。此值根据ECMA-182标准计算得出。
System.out.println(appendObjectResult.getObjectCRC());
System.out.println(JSONObject.toJSONString(appendObjectResult));
// 关闭OSSClient。
ossClient.shutdown();
LOG.info("保存文件记录开始");
fileDto.setPath(path);
fileService.save(fileDto);
ResponseDto responseDto = new ResponseDto();
fileDto.setPath(ossDomain + path);
responseDto.setContent(fileDto);
return responseDto;
}