文件分片上传(支持续传)

                                                      大文件分片上传

 

 

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

 

 

 

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