文件分片上傳(支持續傳)

                                                      大文件分片上傳

 

 

參考博客: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;
}

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章