目录
前言
一、完成的现象
二、核心代码
1.前端核心代码
2.后端核心代码
三、代码下载地址
总结
文件上传是一个老生常谈的话题了,在文件相对比较小的情况下,可以直接把文件转化为字节流上传到服务器,但在文件比较大的情况下,用普通的方式进行上传,这可不是一个好的办法
大文件上传,要解决的问题:
1.文件太大,请求限制,2.文件太大,耗时太久,连接超时
解决方案:采用分片的方式进行上传
前端用百度开源的WebUploader进行分片异步上传
后端用minio的putObject上传,composeObject合并,完成大文件的上传
WebUploader 是由 Baidu WebFE (FEX) 团队开发的一个简单的以 HTML5 为主,FLASH 为辅的现代文件上传组件
// HOOK 这个必须要再uploader实例化前面
WebUploader.Uploader.register({
'before-send-file': 'beforeSendFile',
'before-send': 'beforeSend'
}, {
beforeSendFile: function (file) {
console.log("beforeSendFile");
// Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。
var task = new $.Deferred();
// 根据文件内容来查询MD5
uploader.md5File(file).progress(function (percentage) { // 及时显示进度
console.log('计算md5进度:', percentage);
getProgressBar(file, percentage, "MD5", "MD5进度");
}).then(function (val) { // 完成
console.log('md5 result:', val);
file.md5 = val;
// 模拟用户id
// file.uid = new Date().getTime() + "_" + Math.random() * 100;
file.uid = WebUploader.Base.guid();
// 进行md5判断
$.post("index/checkFileMd5", {uid: file.uid, md5: file.md5},
function (data) {
console.log(data.status);
var status = data.status.value;
task.resolve();
if (status == 101) {
// 文件不存在,那就正常流程
} else if (status == 100) {
// 忽略上传过程,直接标识上传成功;
uploader.skipFile(file);
file.pass = true;
} else if (status == 102) {
// 部分已经上传到服务器了,但是差几个模块。
file.missChunks = data.data;
}
});
});
return $.when(task);
},
beforeSend: function (block) {
console.log("block")
var task = new $.Deferred();
var file = block.file;
var missChunks = file.missChunks;
var blockChunk = block.chunk;
console.log("当前分块:" + blockChunk);
console.log("missChunks:" + missChunks);
if (missChunks !== null && missChunks !== undefined && missChunks !== '') {
var flag = true;
for (var i = 0; i < missChunks.length; i++) {
if (blockChunk == missChunks[i]) {
console.log(file.name + ":" + blockChunk + ":还没上传,现在上传去吧。");
flag = false;
break;
}
}
if (flag) {
task.reject();
} else {
task.resolve();
}
} else {
task.resolve();
}
return $.when(task);
}
});
// 实例化
var uploader = WebUploader.create({
pick: {
id: '#picker',
label: '点击选择文件'
},
formData: {
uid: 0,
md5: '',
chunkSize: chunkSize
},
//dnd: '#dndArea',
//paste: '#uploader',
swf: 'js/Uploader.swf',
chunked: true,
chunkSize: chunkSize, // 字节 5M分块
threads: 5,
server: 'index/fileUpload',
auto: true,
// 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
disableGlobalDnd: true,
fileNumLimit: 1024,
fileSizeLimit: 1024 * 1024 * 1024, // 200 M
fileSingleSizeLimit: 1024 * 1024 * 1024 // 50 M
});
// 当有文件被添加进队列的时候
uploader.on('fileQueued', function (file) {
console.log("fileQueued");
$thelist.append('' +
'' + file.name + '
' +
'等待上传...
' +
'');
});
// 上传中
uploader.on('uploadProgress', function (file, percentage) {
getProgressBar(file, percentage, "FILE", "上传进度");
});
// 上传返回结果
uploader.on('uploadSuccess', function (file) {
var text = '已上传';
if (file.pass) {
text = "文件妙传功能,文件已上传。"
}
$('#' + file.id).find('p.state').text(text);
});
uploader.on('uploadError', function (file) {
$('#' + file.id).find('p.state').text('上传出错');
});
uploader.on('uploadComplete', function (file) {
// 隐藏进度条
fadeOutProgress(file, 'MD5');
// fadeOutProgress(file, 'FILE');
});
// 文件上传
$btn.on('click', function () {
console.log("上传...");
uploader.upload();
console.log("上传成功");
});
/**
* 生成进度条封装方法
* @param file 文件
* @param percentage 进度值
* @param id_Prefix id前缀
* @param titleName 标题名
*/
function getProgressBar(file, percentage, id_Prefix, titleName) {
var $li = $('#' + file.id), $percent = $li.find('#' + id_Prefix + '-progress-bar');
// 避免重复创建
if (!$percent.length) {
$percent = $('' +
''
).appendTo($li).find('#' + id_Prefix + '-progress-bar');
}
var progressPercentage = Math.ceil(percentage * 100) + '%';
$percent.css('width', progressPercentage);
$percent.html(titleName + ':' + progressPercentage);
}
' +
'
MinIO提供高性能、S3兼容的对象存储。Minio 是一个基于Go语言的对象存储服务。它实现了大部分亚马逊S3云存储服务接口,可以看做是是S3的开源版本,非常适合于存储大容量非结构化的数据,例如图片、视频、日志文件、备份数据和容器/虚拟机镜像等
@Override
public void uploadFileToMinio(MultipartFileParam param) throws Exception {
String md5 = param.getMd5();
//总分片数
int totalPieces = param.getChunks();
//当前分片
int sliceIndex = param.getChunk();
// 可以最后一个分片传文件名用来保存
String fileName = param.getName();
//上传文件
MultipartFile file = param.getFile();
// 上传
minioClient.putObject(
PutObjectArgs.builder()
.bucket(minIoClientConfig.getTmpBucketName())
.object(md5.concat("/").concat(Integer.toString(sliceIndex)))
.stream(file.getInputStream(), file.getSize(), -1)
.contentType(file.getContentType())
.build());
//判断是否全部上传完成
if(isUploadComplete(md5,totalPieces)) {
log.info("上传临时目录完成--------------");
//合并文件
mergeFile(totalPieces,md5,fileName);
// 删除所有的分片文件
deleteTempFile(totalPieces,md5);
log.info("完成上传");
}
}
/**
* 删除临时文件
* @param totalPieces
* @param md5
*/
private void deleteTempFile(int totalPieces,String md5) {
List delObjects = Stream.iterate(0, i -> ++i)
.limit(totalPieces)
.map(i -> new DeleteObject(md5.concat("/").concat(Integer.toString(i))))
.collect(Collectors.toList());
log.info("删除临时分片:{}",delObjects);
minioClient.removeObjects(
RemoveObjectsArgs.builder().bucket(minIoClientConfig.getTmpBucketName())
.objects(delObjects).build());
}
/**
* 合并文件
* @param totalPieces
* @param md5
* @param fileName
* @throws Exception
*/
private void mergeFile(int totalPieces,String md5,String fileName) throws Exception{
// 完成上传从缓存目录合并迁移到正式目录
List sourceObjectList = Stream.iterate(0, i -> ++i)
.limit(totalPieces)
.map(i -> ComposeSource.builder()
.bucket(minIoClientConfig.getTmpBucketName())
.object(md5.concat("/").concat(Integer.toString(i)))
.build())
.collect(Collectors.toList());
minioClient.composeObject(
ComposeObjectArgs.builder()
.bucket(minIoClientConfig.getBucketName())
.object(fileName)
.sources(sourceObjectList)
.build());
}
/**
* 是否上传完成
* @param md5
* @param totalPieces
* @return
* @throws Exception
*/
private boolean isUploadComplete(String md5,int totalPieces) throws Exception{
Iterable> results = minioClient.listObjects(
ListObjectsArgs.builder().bucket(minIoClientConfig.getTmpBucketName())
.prefix(md5.concat("/")).build());
Set objectNames = Sets.newHashSet();
for (Result- item : results) {
objectNames.add(item.get().objectName());
}
return objectNames.size()==totalPieces;
}
代码:https://gitee.com/zenglx/big-file-upload.git
当我们在做文件上传的功能时,如果上传的文件过大,可能会导致长传时间特别长,且上传失败后需要整个文件全部重新上传。因此,我们需要前后端配合来解决这个问题。
最常用的解决方案就是 —— 切片上传。
文件切片上传
文件秒传
文件断点续传