java 实现文件秒传_大文件批量上传断点续传文件秒传

接上篇文章 java 超大文件分片上传 在其基础上继续实现 断点续传和文件秒传功能

在上篇中,我们可以使用 file. slice 方法对文件进行分片,可以从后台读到当前文件已经上传的大小,就可以知道从哪里开始切片,断点续传的原理就是基于这个的。

前端计算文件的 md5 ,后台数据库查询一遍(前提是把 md5 存储了,计算文件 md5 也是需要消耗时间的)即可知道是否有相同文件,这是实现文件秒传的方法。

可能存在的问题:

有两个人同时在上传同一个文件,但前一个人还没有上传完成,此时第二个文件认为是新文件不能秒传

此时获取文件原数据时需要将文件信息保存起来,重点是要保存 md5 ,保证一个文件的 md5 保计算一次

获取断点文件时,真实的文件上传位置应该是从文件系统中读出来的

根据需求说明,后台应该存在四个接口,获取文件信息(包含是否可以秒传),获取断点文件列表,分片上传接口,文件完整性验证

/**

* 加载断点文件列表

* @return

*/

@GetMapping("/breakPointFiles")

public List breakPointFiles(){

List fileInfoPos = fileMetaDataRepository.breakPointFiles();

return fileInfoPos;

}

/**

* 获取文件元数据,判断文件是否可以秒传

* @param originFileName

* @param fileSize

* @param md5

* @return

* @throws URISyntaxException

*/

@GetMapping("/fileMetaData")

public FileMetaData fileMetaData(String originFileName, Long fileSize, String md5) throws URISyntaxException, MalformedURLException {

FileMetaData similarFile = bigFileStorage.checkSimilarFile(originFileName,fileSize, md5);

if(similarFile != null){

similarFile.setSecUpload(true);

// 如果文件名不一致,则创建链接文件

if(!similarFile.getOriginFileName() .equals(originFileName)) {

bigFileStorage.createSimilarLink(similarFile);

}

return similarFile;

}

//获取文件相关信息

String baseName = FilenameUtils.getBaseName(originFileName);

String extension = FilenameUtils.getExtension(originFileName);

String finalFileName = bigFileStorage.rename(baseName, fileSize);

if(StringUtils.isNotEmpty(extension)){

finalFileName += ("."+extension);

}

URI relativePath = bigFileStorage.relativePath(finalFileName);

//如果没有相似文件,则要创建记录到数据库中,为后面断点续传做准备

FileInfoPo fileInfoPo = new FileInfoPo();

fileInfoPo.setName(originFileName);

fileInfoPo.setType(extension);

fileInfoPo.setUploaded(0);

fileInfoPo.setSize(fileSize);

fileInfoPo.setRelativePath(relativePath.toString());

fileInfoPo.setMd5(md5);

fileMetaDataRepository.insert(fileInfoPo);

URI absoluteURI = bigFileStorage.absolutePath(relativePath);

FileMetaData fileMetaData = new FileMetaData(originFileName, finalFileName, fileSize, relativePath.toString(), absoluteURI.toString());

fileMetaData.setMd5(md5);

fileMetaData.setFileType(extension);

return fileMetaData;

}

/**

* 获取当前文件已经上传的大小,用于断点续传

* @return

*/

@GetMapping("/filePosition")

public long filePosition(String relativePath) throws IOException, URISyntaxException {

return bigFileStorage.filePosition(relativePath);

}

/**

* 上传分段

* @param multipartFile

* @return

*/

@PostMapping("/uploadPart")

public long uploadPart(@RequestParam("file") MultipartFile multipartFile, String relativePath) throws IOException, URISyntaxException {

bigFileStorage.uploadPart(multipartFile,relativePath);

return bigFileStorage.filePosition(relativePath);

}

/**

* 检查文件是否完整

* @param relativePath

* @param fileSize

* @param md5

* @return

*/

@GetMapping("/checkIntegrity")

public void checkIntegrity(String relativePath,Long fileSize,String fileName) throws IOException, URISyntaxException {

long filePosition = bigFileStorage.filePosition(relativePath);

Assert.isTrue(filePosition == fileSize ,"大文件上传失败,文件大小不完整 "+filePosition+" != "+fileSize);

String targetMd5 = bigFileStorage.md5(relativePath);

FileInfoPo fileInfoPo = fileMetaDataRepository.selectByPrimaryKey(fileName);

String md5 = fileInfoPo.getMd5();

Assert.isTrue(targetMd5.equals(md5),"大文件上传失败,文件损坏 "+targetMd5+" != "+md5);

//如果文件上传成功,更新文件上传大小

fileMetaDataRepository.updateFilePosition(fileName,filePosition);

}

重要的处理部分其实还是前端,下面看前端的代码,需要使用到一个计算 md5 值的库 spark-md5.js

大文件批量上传,支持断点续传,文件秒传

.upload-item{

padding: 15px 10px;

list-style-type: none;

display: flex;

flex-direction: row;

margin-bottom: 10px;

border: 1px dotted lightgray;

width: 1000px;

position: relative;

}

.upload-item:before{

content: ' ';

background-color: lightblue;

width: 0px;

position: absolute;

left: 0;

top: 0;

bottom: 0;

z-index: -1;

}

.upload-item span{

display: block;

margin-left: 20px;

}

.upload-item>.file-name{

width: 200px;

overflow: hidden;

text-overflow: ellipsis;

white-space: nowrap;

}

.upload-item>.upload-process{

width: 50px;

text-align: left;

}

.upload-item>.upload-status{

width: 100px;

text-align: center;

}

table{

width: 100%;

border-collapse: collapse;

position: fixed;

bottom: 200px;

border: 1px solid whitesmoke;

}

开始上传

文件名文件大小已上传大小相对路径md5

const root = '';

const breakPointFiles = root + '/breakPointFiles'; // 获取断点文件列表

const fileMetaData = root + '/fileMetaData'; // 新上传文件元数据,secUpload 属性用于判断是否可以秒传

const uploadPart = root +'/uploadPart'; // 分片上传,每片的上传接口

const checkIntegrity = root + '/checkIntegrity'; // 检查文件完整性

const fileInfoPos = root + '/fileInfoPos'; // 获取系统中所有已经上传的文件(调试)

const shardSize = 1024 * 1024 * 2; // 分片上传,每片大小 2M

const chunkSize = 1024 * 1024 * 4; // md5 计算每段大小 4M

const statusInfoMap = {'0':'待上传','1':'正在计算','2':'正在上传','3':'上传成功','4':'上传失败','5':'暂停上传','6':'文件检查'};

let uploadFiles = {}; //用于存储当前需要上传的文件列表 fileName=>fileInfo

$(function () {

// 用于调试 begin 加载系统中已经上传过的文件列表

$.ajax({

type:'get',

url:fileInfoPos,

dataType:'json',

success:function (res) {

let htmlCodes = [];

for(let i=0;i

htmlCodes.push('

');

htmlCodes.push('

'+res[i].name+'');

htmlCodes.push('

'+res[i].size+'');

htmlCodes.push('

'+res[i].uploaded+'');

htmlCodes.push('

'+res[i].relativePath+'');

htmlCodes.push('

'+res[i].md5+'');

htmlCodes.push('

')

}

$('table').append(htmlCodes.join(''))

}

})

// 用于调试 end

// 事件绑定

$('#file').change(changeFiles); // 选择文件列表事件

$('#startUpload').click(beginUpload); // 开始上传

$('#uploadfiles').on('change','input[type=file]',breakPointFileChange); // 断点文件选择事件

// 初始化时加载断点文件

(function () {

$.ajax({

type:'get',

url:breakPointFiles,

dataType:'json',

success:function (files) {

if(files && files.length > 0){

for (let i=0;i

let fileId = id();

let process = parseFloat((files[i].uploaded / files[i].size ) * 100).toFixed(2);

$('#uploadfiles').append(templateUploadItem(fileId,files[i],process,5,'断点续传',i+1));

uploadFiles[fileId] = {fileInfo:files[i],status:5};

}

}

}

})

})(window);

/**

* 文件重新选择事件

* @param e

*/

function changeFiles(e) {

// 检测文件列表是否符合要求,默认都符合

if(this.files.length == 0){return ;}

// 先把文件信息追加上去,不做检查也不上传

for (let i = 0; i < this.files.length; i++) {

let file = this.files[i];

let fileId = id();

$('#uploadfiles').append(templateUploadItem(fileId,file,0,0,''));

uploadFiles[fileId] = {file:file,status:0};

}

}

/**

* 断点文件选择文件事件

*/

function breakPointFileChange(e) {

let fileId = $(e.target).closest('li').attr('fileId');

if(this.files.length > 0){

uploadFiles[fileId].file = this.files[0];

}

}

/**

* 开始上传

*/

function beginUpload() {

// 先对每一个文件进行检查,除断点文件不需要检查外

// console.log(uploadFiles);

for(let fileId in uploadFiles){

// 如果断点文件没有 file 信息,直接失败

if(uploadFiles[fileId].status == 5 && !uploadFiles[fileId].file){

//断点文件一定有 fileInfo

let fileInfo = uploadFiles[fileId].fileInfo;

let $li = $('#uploadfiles').find('li[fileId='+fileId+']');

$li.children('.upload-status').text('上传失败');fileInfo.status = 4;

$li.children('.tips').text('无文件信息');

continue;

}

if(uploadFiles[fileId].status == 5){

//如果断点文件有 file 信息,则可以直接断点续传了

let $li = $('#uploadfiles').find('li[fileId='+fileId+']');

$li.children('.upload-status').text('正在上传');uploadFiles[fileId].status = 2;

startUpload(uploadFiles[fileId],$li);

continue;

}

//其它待上传的文件,先后台检查文件信息,再上传

if(uploadFiles[fileId].status == 0){

let $li = $('#uploadfiles').find('li[fileId='+fileId+']');

uploadFiles[fileId].status = 1; $li.children('.upload-status').text('正在计算') //正在计算

checkFileItem(uploadFiles[fileId].file,function (res) {

if(res.message && res.message == 'fail'){

$li.children('.upload-status').text(res.returnCode || '上传出错');uploadFiles[fileId].status = 4;

}else{

uploadFiles[fileId].fileInfo = res;

if(res.secUpload){

$li.children('.upload-status').text('文件秒传');uploadFiles[fileId].status = 3;

$li.children('.upload-process').text('100 %');

}else{

$li.children('.upload-status').text('正在上传');uploadFiles[fileId].status = 2;

startUpload(uploadFiles[fileId],$li);

}

}

});

}

}

/**

* 计算 md5 值,请求后台查看是否可秒传

*/

function checkFileItem(file,callback) {

md5Hex(file,function (md5) {

$.ajax({

type:'get',

async:false,

url:fileMetaData,

data:{originFileName:file.name,fileSize:file.size,md5:md5},

dataType:'json',

success:callback

});

});

}

/**

* 开始正式上传单个文件

* */

function startUpload(uploadFile,$li) {

let file = uploadFile.file;

let offset = uploadFile.fileInfo.uploaded || 0;

let shardCount =Math.ceil((file.size - offset )/shardSize);

for(var i=0;i

var start = i * shardSize + offset;

var end = Math.min(file.size,start + shardSize );//在file.size和start+shardSize中取最小值,避免切片越界

var filePart = file.slice(start,end);

var formData = new FormData();

formData.append("file",filePart,uploadFile.fileInfo.name || uploadFile.fileInfo.originFileName);

formData.append('relativePath',uploadFile.fileInfo.relativePath);

$.ajax({

async:false,

url: uploadPart,

cache: false,

type: "POST",

data: formData,

dateType: 'json',

processData: false,

contentType: false,

success:function (uploaded) {

//进度计算

let process = parseFloat((uploaded / file.size) * 100).toFixed(2);

console.log(file.name+'|'+process);

$li.find('.upload-process').text(process + '%');

// 视觉进度

// $('.upload-item').append("");

if(uploaded == file.size){

// 上传完成后,检查文件完整性

$li.children('.upload-status').text('文件检查');

$.ajax({

type:'get',

async:false,

url:checkIntegrity,

data:{fileName:uploadFile.fileInfo.name || uploadFile.fileInfo.originFileName,fileSize:uploaded,relativePath:uploadFile.fileInfo.relativePath},

success:function (res) {

if(res.message != 'fail'){

$li.children('.upload-status').text('上传成功');

}else{

$li.children('.upload-status').text('上传失败');

$li.children('.tips').text(res.returnCode);

}

}

})

}

}

});

}

}

}

/**

* 创建模板 html 上传文件项

* @param fileName

* @param process

* @param status

* @param tips

* @returns {string}

*/

function templateUploadItem(fileId,fileInfo,process,status,tips,breakPoint) {

let htmlCodes = [];

htmlCodes.push('

');

htmlCodes.push(''+(fileInfo.name || fileInfo.originFileName)+'');

htmlCodes.push(''+(fileInfo.size)+'');

htmlCodes.push(''+process+' %');

htmlCodes.push(''+statusInfoMap[status+'']+'');

htmlCodes.push(''+tips+'');

if(breakPoint){

htmlCodes.push('');

}

htmlCodes.push('');

return htmlCodes.join('');

}

/**

* 计算 md5 值(同步计算)

* @param file

*/

function md5Hex(file,callback) {

let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,

chunks = Math.ceil(file.size / chunkSize),

currentChunk = 0,

spark = new SparkMD5.ArrayBuffer(),

fileReader = new FileReader();

fileReader.onload = function (e) {

spark.append(e.target.result); // Append array buffer

currentChunk++;

if (currentChunk < chunks) {

loadNext();

} else {

let hash = spark.end();

callback(hash);

}

}

fileReader.onerror = function () {

console.warn('md5 计算时出错');

};

function loadNext(){

var start = currentChunk * chunkSize,

end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;

fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));

}

loadNext();

}

function id() {

return Math.floor(Math.random() * 1000);

}

});

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值