大文件分片上傳
參考博客:https://blog.csdn.net/haohao123nana/article/details/54692669
分片上傳的意義在於上傳內容較大的文件時,如果出現網絡錯誤,普通上傳只能重新開始上傳,但是分片上傳可以從中斷的那個分片繼續上傳,給力!!!
demo效果圖:
思路圖解:
前提摘要:
☛☛ 文件的狀態保存在mysql數據庫中,當上傳第一個分片時,將文件記錄插入數據庫,標記狀態爲0(未傳完),文件上傳合併之後,修改狀態爲1(上傳完成)
表結構如下:
☛☛ 當發現文件之前上傳過,首先要獲取之前上傳的所有文件分片,比如之前上傳到第5個分片時出錯,文件夾下有五個分片,這時候需要刪除第五個分片,然後從第五個分片重新開始傳,因爲不能確定之前傳的第五個分片到底完不完整。
OK:直接上代碼吧
前端html:
<body>
<input type="file" id="file" />
<button id="upload">上傳</button>
<br>
<br>
<p>
文件上傳的狀態:<span id="state">未上傳</span>
</p>
<p>
文件總分片數量:<span id="count">?</span>
</p>
<p>
文件上傳總耗時:<span id="useTime">0</span>S
</p>
</body>
JS代碼:
<script type="text/javascript">
var i;
var databgein; //開始時間
$("#upload").click(function() {
$("#state").html("上傳中...")
$("#useTime").html(0);
$("#count").html("?");
// 設置初始值
i = -1;
// 初始化開始時間
databgein = new Date().getTime();
//文件對象
var file = $("#file")[0].files[0];
// 判斷文件是否存在
if (file == null)
alert("請選擇上傳文件");
// 驗證文件是否上傳過
isUpload(file);
});
function isUpload(file) {
// 構造一個form表單
var form = new FormData();
// 拼接參數:文件名稱
form.append("fileName", file.name);
// 判斷是否上傳
$.ajax({
url : "http://localhost:8081/demo/file/checkFile",
type : "POST",
data : form,
async : true, //異步
processData : false, //很重要,告訴jquery不要對form進行處理
contentType : false, //很重要,指定爲false才能形成正確的Content-Type
success : function(data) {
var update = data.update;
var filemd5 = data.filemd5;
// 文件未上傳過
if (data.flag == 1)
upload(file, filemd5, update, false);
// 文件上傳過,但是沒有傳完
if (data.flag == 2)
upload(file, filemd5, update, true);
// 秒傳
if (data.flag == 3) {
console.log("文件秒傳");
$("#state").html("上傳完畢")
}
},
error : function() {
alert("服務器出錯!");
}
})
}
function upload(file, filemd5, update, action) {
// 獲取文件名
name = file.name;
// 獲取文件總大小
size = file.size;
// 以5M爲一個分片
var shardSize = 5 * 1024 * 1024;
// 計算總分片數量
var shardCount = Math.ceil(size / shardSize);
$("#count").html(shardCount + "");
// 下一個分片
i += 1;
// 計算每一片的起始與結束位置
var start = i * shardSize;
var end = Math.min(size, start + shardSize);
// 構建表單
var form = new FormData();
// 文件上傳狀態
form.append("action", action);
// 切割文件分片
form.append("data", file.slice(start, end));
// 整個文件的MD5
form.append("filemd5", filemd5);
// 上傳日期
form.append("update", update);
// 文件名稱
form.append("name", name);
// 總分片數量
form.append("total", shardCount); //總片數
// 當前上傳的是第幾個分片
form.append("index", i + 1); //當前是第幾片
$("#index").html(i);
$.ajax({
url : "http://localhost:8081/demo/file/upload",
type : "POST",
data : form,
async : false, //異步
processData : false, //很重要,告訴jquery不要對form進行處理
contentType : false, //很重要,指定爲false才能形成正確的Content-Type
success : function(data) {
// 如果文件之前上傳過,直接從index片繼續上傳
if (action) {
i = data.index;
action = false;
}
//服務器返回分片是否上傳成功
if (data.index == shardCount) {
var time = new Date().getTime() - databgein;
$("#useTime").html(time / 1000);
$("#state").html("上傳完畢");
return;
}
upload(file, filemd5, update, action);
},
error : function(XMLHttpRequest, textStatus, errorThrown) {
alert("服務器出錯!");
}
})
}
</script>
Controller代碼:
/**
* 檢驗文件是否上傳過
*
* @param request
* @return
*/
@PostMapping("/checkFile")
@ResponseBody
public ResponseEntity<Map<String, Object>> checkFile(
HttpServletRequest request) {
try {
// 獲取文件名稱
String fileName = request.getParameter("fileName");
// 使用spring自帶的md5加密工具對文件名加密
String filemd5 = DigestUtils.md5DigestAsHex(fileName.getBytes());
// 驗證文件是否上傳過
Record fileRes = uploadService.getFileByMd5(filemd5);
// 創建返回數據容器
Map<String, Object> map = new HashMap<String, Object>();
// 沒有上傳過文件
if (fileRes == null) {
map.put("flag", Constant.NO_UPLOAD);
map.put("update", CommonUtils.format(new Date()));
map.put("filemd5", filemd5);
return new ResponseEntity<Map<String, Object>>(map,
HttpStatus.OK);
}
// 文件已上傳,但是還沒上傳完
if (fileRes.getStatus() == 0) {
map.put("flag", Constant.NO_END_UPLOAD);
map.put("update", CommonUtils.format(fileRes.getUpDate()));
map.put("filemd5", filemd5);
return new ResponseEntity<Map<String, Object>>(map,
HttpStatus.OK);
}
// 文件已上傳
map.put("flag", Constant.END_UPLOAD);
return new ResponseEntity<Map<String, Object>>(map, HttpStatus.OK);
} catch (Exception e) {
log.info("驗證文件是否上傳失敗" + e);
return new ResponseEntity<Map<String, Object>>(
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@PostMapping("/upload")
@ResponseBody
public ResponseEntity<Map<String, Object>> uplaod(
HttpServletRequest request,
@RequestParam(value = "data", required = false) MultipartFile multipartFile) {
// 文件名
String fileName = request.getParameter("name");
// 總片數
int total = Integer.valueOf(request.getParameter("total"));
// 完整文件的md5
String filemd5 = request.getParameter("filemd5");
// 第一個分片上傳的日期(如:20170122)
String update = request.getParameter("update");
// 當前是第幾片
int index = Integer.valueOf(request.getParameter("index"));
// 判斷文件是否上傳過
boolean action = Boolean.valueOf(request.getParameter("action"))
.booleanValue();
// 創建返回數據容器
Map<String, Object> map = new HashMap<String, Object>();
try {
// 獲取tomcat下的webapp目錄
String webapp = request.getSession().getServletContext()
.getRealPath("/");
// 按日期生成文件保存目錄
String filePath = webapp + "files" + File.separator + update;
// 分片上傳保存路徑,以文件名稱加密的md5字符串當目錄
// String shardDir = CommonUtils.getFileNameNoEx(fileName);
String shardPath = filePath + File.separator + filemd5;
// 驗證路徑是否存在,不存在則創建目錄
File path = new File(shardPath);
if (!path.exists()) {
path.mkdirs();
}
// 當action = true時,文件之前上傳過
if (action) {
// 獲取之前上傳的分片文件的數量
int len = path.listFiles().length;
// 獲取最後一個文件分片,並刪除(上次傳輸過程中可能出現錯誤)
File file = new File(shardPath, filemd5 + "_" + len);
if (file.exists()) {
file.delete();
}
// 返回並指定從第幾片開始上傳
map.put("index", len - 2);
return new ResponseEntity<Map<String, Object>>(map,
HttpStatus.OK);
}
// 上傳分片
multipartFile
.transferTo(new File(shardPath, filemd5 + "_" + index));
// 上傳第一個分片,並記錄文件到數據庫
if (index == Constant.ONE) {
// 驗證是否有記錄
Record record = uploadService.getFileByMd5(filemd5);
if (record == null) {
// 獲取文件後綴
String suffix = CommonUtils.getExtensionName(fileName);
// 拼接url
String url = filePath + File.separator + filemd5 + "."
+ suffix;
// 保存到數據庫
Record r = new Record();
r.setMd5(filemd5);
r.setStatus(Constant.ZERO);
r.setUrl(url);
r.setUpDate(new Date());
uploadService.addFile(r);
}
}
// 獲取文件夾下的文件數量
File[] fileArray = path.listFiles();
// 比較文件數量和分片數量是否相等,相等意味着上傳完成,開始合併
if (fileArray != null && fileArray.length == total) {
// 獲取文件後綴
String suffix = CommonUtils.getExtensionName(fileName);
File newFile = new File(filePath, filemd5 + "." + suffix);
// 文件追加寫入
FileOutputStream outputStream = new FileOutputStream(newFile,
true);
// 讀取分片文件內容,合併成一個文件
byte[] byt = new byte[10 * 1024 * 1024];
int len;
FileInputStream temp = null;// 分片文件
for (int i = 0; i < total; i++) {
int j = i + 1;
temp = new FileInputStream(new File(shardPath, filemd5
+ "_" + j));
while ((len = temp.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
}
// 關閉流
temp.close();
outputStream.close();
// 更新文件上傳的狀態爲完成
uploadService.updStatus(filemd5, Constant.ONE);
// 刪除文件夾
CommonUtils.deleteDir(shardPath);
}
// 返回
map.put("index", index);
return new ResponseEntity<Map<String, Object>>(map, HttpStatus.OK);
} catch (Exception e) {
log.info("大文件上傳失敗:" + e);
return new ResponseEntity<Map<String, Object>>(
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Pojo類:
import java.util.Date;
import lombok.Data;
@Data
public class Record {
// 主鍵
private Integer id;
// 文件存儲路徑
private String url;
// 上傳日期
private Date upDate;
// 文件md5值
private String md5;
// 文件上傳狀態
private Integer status;
}
Utils類:
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
public class CommonUtils {
/**
* 獲取文件的拓展名
*
* @param filename
* @return
*/
public static String getExtensionName(String filename) {
if ((filename != null) && (filename.length() > 0)) {
int dot = filename.indexOf('.');
if ((dot > -1) && (dot < (filename.length() - 1))) {
return filename.substring(dot + 1);
}
}
return filename.toLowerCase();
}
/**
* 獲取 YYYYMMDD格式的日期
*
* @param date
* @return
*/
public static String format(Date date) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
return sdf.format(date);
}
/**
* 刪除文件或文件夾
*
* @param dirPath
*/
public static void deleteDir(String path) {
File file = new File(path);
if (null != file) {
if (!file.exists()) {
return;
}
int i;
// file 是文件
if (file.isFile()) {
boolean result = file.delete();
// 限制循環次數,避免死循環
for (i = 0; !result && i++ < 10; result = file.delete()) {
// 垃圾回收
System.gc();
}
return;
}
// file 是目錄
File[] files = file.listFiles();
if (null != files) {
for (i = 0; i < files.length; ++i) {
deleteDir(files[i].getAbsolutePath());
}
}
file.delete();
}
}
}
public class Constant {
public static int NO_UPLOAD = 1; // 沒有上傳
public static int NO_END_UPLOAD = 2; // 上傳,但是沒結束
public static int END_UPLOAD = 3; // 上傳結束
public static int ZERO = 0;
public static int ONE = 1;
}