接上篇文章 java 超大文件分片上傳 在其基礎上繼續實現 斷點續傳和文件秒傳功能
在上篇中,我們可以使用 file. slice 方法對文件進行分片,可以從后台讀到當前文件已經上傳的大小,就可以知道從哪里開始切片,斷點續傳的原理就是基於這個的。
前端計算文件的 md5 ,后台數據庫查詢一遍(前提是把 md5 存儲了,計算文件 md5 也是需要消耗時間的)即可知道是否有相同文件,這是實現文件秒傳的方法。
可能存在的問題:
- 有兩個人同時在上傳同一個文件,但前一個人還沒有上傳完成,此時第二個文件認為是新文件不能秒傳
- 此時獲取文件原數據時需要將文件信息保存起來,重點是要保存 md5 ,保證一個文件的 md5 保計算一次
- 獲取斷點文件時,真實的文件上傳位置應該是從文件系統中讀出來的
根據需求說明,后台應該存在四個接口,獲取文件信息(包含是否可以秒傳),獲取斷點文件列表,分片上傳接口,文件完整性驗證
全部源碼位置 : https://gitee.com/sanri/example/tree/master/test-mvc
/**
* 加載斷點文件列表
* @return
*/
@GetMapping("/breakPointFiles")
public List<FileInfoPo> breakPointFiles(){
List<FileInfoPo> 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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>大文件批量上傳,支持斷點續傳,文件秒傳</title>
<style>
.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;
}
</style>
</head>
<body>
<div class="file-uploads">
<input type="file" multiple id="file" />
<button id="startUpload">開始上傳</button>
<ul id="uploadfiles">
</ul>
<table class="" style="" id="table" >
<thead>
<tr>
<td>文件名</td>
<td>文件大小</td>
<td>已上傳大小</td>
<td>相對路徑</td>
<td>md5</td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<!-- <script src="jquery-1.8.3.min.js"></script>-->
<script src="jquery1.11.1.min.js"></script>
<script src="spark-md5.min.js"></script>
<script>
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<res.length;i++){
htmlCodes.push('<tr>');
htmlCodes.push('<td>'+res[i].name+'</td>');
htmlCodes.push('<td>'+res[i].size+'</td>');
htmlCodes.push('<td>'+res[i].uploaded+'</td>');
htmlCodes.push('<td>'+res[i].relativePath+'</td>');
htmlCodes.push('<td>'+res[i].md5+'</td>');
htmlCodes.push('</tr>')
}
$('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<files.length;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<shardCount;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("<style>.upload-item::before{ width:"+(process * 1000)+ "% }</style>");
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('<li class="upload-item" fileId="'+fileId+'">');
htmlCodes.push('<span class="file-name">'+(fileInfo.name || fileInfo.originFileName)+'</span>');
htmlCodes.push('<span class="file-size">'+(fileInfo.size)+'</span>');
htmlCodes.push('<span class="upload-process">'+process+' %</span>');
htmlCodes.push('<span class="upload-status" >'+statusInfoMap[status+'']+'</span>');
htmlCodes.push('<span class="tips">'+tips+'</span>');
if(breakPoint){
htmlCodes.push('<input type="file" name="file" style="margin-left: 10px;"/>');
}
htmlCodes.push('</li>');
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);
}
});
</script>
</body>
</html>
源碼位置: https://gitee.com/sanri/example/tree/master/test-mvc
一點小推廣
創作不易,希望可以支持下我的開源軟件,及我的小工具,歡迎來 gitee 點星,fork ,提 bug 。
Excel 通用導入導出,支持 Excel 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi
使用模板代碼 ,從數據庫生成代碼 ,及一些項目中經常可以用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven
