先说说大文件上传种用的点以及原理,也希望各位指正。
思路以及大部分源码来自于 haohao123naa博主,我只是在他的基础上进行完善点击打开链接,在此表示感谢。
1.文件分块:一个超过几个G的文件上传到服务器,如果你只使用最简单上传、接收、处理、还能成功的话,我只能说你家服务器真好。就算服务器够好,这种操作也时不被允许的。所以我们要想办法解决这种困难。
首先我们要解决大文件的问题,没有办法只能切成几M的 byte小文件多次发送至服务器,并保存下来。然后给这些文件用源文件的MD5+index进行命名,当然也有朋友用UUID+index进行命名,这两种的区别会在下文详细讲述。当你把这些小文件分别上传到服务器上时,最好把这些记录保存到数据库。
(1)当第一个分块上传完成时,将源文件的名称、类型、源文件的MD5、上传日期、地址、未完成的状态写入到一张表,等拼接完成改状态为完成。暂时命名为file表
(2)每一个分块上传完成后记录保存到数据库里面,源文件的MD5+index名字、分块的MD5(这是一个重点)、上传时间、文件地址。保存进数据库 命名为file__tem表
2.秒传功能:很多网盘都实现了这个功能,上传开始时发送Ajax请求,查询本次要上传的文件,是否存在,这里我们H5提供了获取文件MD5的方法,然后用ajax去file里请求这个md5是否存在、状态是否时完成,存在的情况下,也要验证本地这个文件是不是还存在这。同时存在的情况下。就可以给前台返回存在状态了,然后你就可以傲娇的告诉客户,秒传了。
3.断点续传:这里就为什么我们为什么不用简单的UUID进行命名,而是使用MD5的原因了,大文件上传失败是很常见的一种情况,现在分成一块块的小文件了,失败了我们就从失败的那个小文件开始继续上传不就可以了。
(1)Ajax 请求file 表、存在记录、状态未完成。(状态完成就是秒传了)
(2)文件分块 Ajax用MD5请求file_temp表是否存在,存在递归下一块的MD5请求,
(3)第二步不存在的情况下、这里再用form提交MultipartFile以及相关信息,上传成功后,写入数据库。
4.所有分块完成拼接到一起,改file表状态。
5.源码部分(本人代码写的不是很好,希望各位指正)
(HTML)
lay-filter="demo" id="progress" style="display: none">
( controller部分)
package com.folkestone.hxwm.controller.base;
import com.folkestone.hxwm.service.commom.ResService;
import com.folkestone.hxwm.service.commom.FileResService;
import com.folkestone.hxwm.bean.bean_dto.common.FileRes;
import com.folkestone.hxwm.common.util.bigfileup.DataUntil;
import com.folkestone.hxwm.common.util.bigfileup.FileMd5Util;
import com.folkestone.hxwm.common.util.bigfileup.FileOpera;
import com.folkestone.hxwm.common.util.bigfileup.NameUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;
/**
* Created by Administrator on 2015/10/9.
*/
@RestController
@RequestMapping(value ="/admin/bigfile")
public class BFileUController {
@Autowired
private FileResService fileResService;
@Autowired
private ResService resService;
/**
* 上传文件
*
* @param request
* @return
* @throws IllegalStateException
* @throws IOException
*/
@RequestMapping(value = "/upload")
public Map upload(
HttpServletRequest request, @RequestParam(value = "data",required = false)
MultipartFile multipartFile) throws IllegalStateException, IOException, Exception {
String action = request.getParameter("action");
String uuid = request.getParameter("uuid");
String fileName = request.getParameter("name");
int size = Integer.valueOf(request.getParameter("size"));//总大小
int total = Integer.valueOf(request.getParameter("total"));//总片数
int index = Integer.valueOf(request.getParameter("index"));//当前是第几片
String fileMd5 = request.getParameter("filemd5"); //整个文件的md5
String date = request.getParameter("date"); //文件第一个分片上传的日期(如:20170122)
String md5 = request.getParameter("md5"); //分片的md5
//文件夹路径
String saveDirectory= FileOpera.CreatePath(date,uuid);
//分片文件
File path = new File(saveDirectory);
if (!path.exists()) {
path.mkdirs();
}
File file = new File(saveDirectory, uuid + "_" + index);
//根据action不同执行不同操作. check:校验分片是否上传过; upload:直接上传分片
Map map = null;
if("check".equals(action)){
String md5Str = FileMd5Util.getFileMD5(file);
//已经上传部分文件 断点续传
if (md5Str != null && md5Str.length() == 31) {
//查询临时表 当前片段MD5是否存在
List li= resService.select(md5,file.getPath());
//查看当前片段是否上传
map = new HashMap<>();
map=FileOpera.FileExit(li);
//返回执行下一段
return map;
}else {
//重新上传一次
map = new HashMap<>();
map.put("flag", "1");
map.put("fileId", uuid);
map.put("status", true);
return map;
}
}else if("upload".equals(action)){
map = new HashMap<>();
int a = -1;
//未上传
//删除文件后上传
if (file.exists()) {
file.delete();
}
multipartFile.transferTo(new File(saveDirectory, uuid + "_" + index));
//添加进数据库
FileRes fileRes = new FileRes();
fileRes.setUuid(uuid);
fileRes.setPath(file.getPath());
fileRes.setSize(size);
fileRes.setMd5(md5);
fileRes.setStatus(1);
fileRes.setCreateTime(DataUntil.Tidata());
a=resService.insert(fileRes);
if(a<0) {//片段失败重新上传
map = new HashMap<>();
map.put("flag", "4");
map.put("fileId", uuid);
map.put("status", false);
return map;
}
}
if (path.isDirectory()) {
File[] fileArray = path.listFiles();
if (fileArray != null) {
if (fileArray.length == total) {
int success=-1;
//分块全部上传完毕,合并
String suffix = NameUtil.getExtensionName(fileName);
File newFile = new File(FileOpera.Createfolder(date), uuid + "." + 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(saveDirectory, uuid + "_" + j));
while ((len = temp.read(byt)) != -1) {
outputStream.write(byt, 0, len);
}
}
//关闭流
temp.close();
outputStream.close();
//修改FileRes记录为上传成功
FileRes fileRes = new FileRes();
fileRes.setStatus(1);
fileRes.setMd5(fileMd5);
fileRes.setPath(newFile.getPath());
fileRes.setSize((int) newFile.length());
fileRes.setSuffix(NameUtil.getExtensionName(fileName));
success =fileResService.update(fileRes);
map=new HashMap<>();
map.put("fileId", uuid);
map.put("flag", "5");
if(success>0) {
map.put("size",fileRes.getSize());
map.put("timelength",fileRes.getTimeLength());
map.put("type",fileRes.getSuffix());
map.put("status", true);
map.put("path",fileRes.getPath());
}else {
map.put("status", false);
}
return map;
}else if(index == 1){
//文件第一个分片上传时记录到数据库
FileRes fileRes = new FileRes();
fileRes.setMd5(fileMd5);
List list = fileResService.selectByMd(fileRes);
String name = NameUtil.getFileNameNoEx(fileName);
if (name.length() > 50) {
name = name.substring(0, 50);
}
fileRes.setName(name);
fileRes.setSuffix(NameUtil.getExtensionName(fileName));
fileRes.setUuid(uuid);
fileRes.setPath(FileOpera.Createfolder(date) + File.separator + uuid + "." + fileRes.getSuffix());
fileRes.setSize(size);
fileRes.setStatus(0);
fileRes.setCreateTime(DataUntil.Tidata());
if (list == null || list.size() == 0) {
this.fileResService.insert(fileRes);
}else {
this.fileResService.update(fileRes);
}
}
}
}
map = new HashMap<>();
map.put("flag", "3");
map.put("fileId", uuid);
map.put("status", true);
return map;
}
/**
* 上传文件前校验
*
* @param request
* @return
* @throws IOException
*/
@RequestMapping(value = "/isUpload")
public Map isUpload(HttpServletRequest request,MultipartFile multipartFile) throws Exception {
String md5 = request.getParameter("md5");
//查看本文件是否存在
FileRes fi = new FileRes();
fi.setMd5(md5);
fi.setStatus(1);
List list = fileResService.selectByMd(fi);
return FileOpera.FileExit(list);
}
}
工具类部分:
package com.folkestone.hxwm.common.util.bigfileup;
import java.io.File;
import java.io.FileInputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
/**
* @author cuihao
* @create 2017-01-20-15:13
*/
public class FileMd5Util {
public static final String KEY_MD5 = "MD5";
public static final String CHARSET_ISO88591 = "ISO-8859-1";
/**
* Get MD5 of one file:hex string,test OK!
*
* @param file
* @return
*/
public static String getFileMD5(File file) {
if (!file.exists() || !file.isFile()) {
return null;
}
MessageDigest digest = null;
FileInputStream in = null;
byte buffer[] = new byte[1024];
int len;
try {
digest = MessageDigest.getInstance("MD5");
in = new FileInputStream(file);
while ((len = in.read(buffer, 0, 1024)) != -1) {
digest.update(buffer, 0, len);
}
in.close();
} catch (Exception e) {
e.printStackTrace();
return null;
}
BigInteger bigInt = new BigInteger(1, digest.digest());
return bigInt.toString(16);
}
/***
* Get MD5 of one file!test ok!
*
* @param filepath
* @return
*/
public static String getFileMD5(String filepath) {
File file = new File(filepath);
return getFileMD5(file);
}
/**
* MD5 encrypt,test ok
*
* @param data
* @return byte[]
* @throws Exception
*/
public static byte[] encryptMD5(byte[] data) throws Exception {
MessageDigest md5 = MessageDigest.getInstance(KEY_MD5);
md5.update(data);
return md5.digest();
}
public static byte[] encryptMD5(String data) throws Exception {
return encryptMD5(data.getBytes(CHARSET_ISO88591));
}
/***
* compare two file by Md5
*
* @param file1
* @param file2
* @return
*/
public static boolean isSameMd5(File file1,File file2){
String md5_1= FileMd5Util.getFileMD5(file1);
String md5_2= FileMd5Util.getFileMD5(file2);
return md5_1.equals(md5_2);
}
/***
* compare two file by Md5
*
* @param filepath1
* @param filepath2
* @return
*/
public static boolean isSameMd5(String filepath1,String filepath2){
File file1=new File(filepath1);
File file2=new File(filepath2);
return isSameMd5(file1, file2);
}
}
package com.folkestone.hxwm.common.util.bigfileup;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import org.springframework.web.multipart.MultipartFile;
import com.folkestone.hxwm.bean.bean_dto.common.FileRes;
import com.folkestone.hxwm.common.util.ContantFinalUtil;
public class FileOpera {
/**
* 根据MD5查看文件是否已经上传
* 文件片段或者文件均可
*
* */
public static Map FileExit(List li) {
Map map = null;
if (li == null || li.size() == 0) {
//没有上传过文件
String uuid = UUID.randomUUID().toString();
map = new HashMap<>();
map.put("flag", "1");
map.put("fileId", uuid);
map.put("date", DataUntil.StrDta());
map.put("status", true);
} else {
FileRes fileRes = li.get(0);
//判断文件是否删除 本地文件是否存在
File file=new File(fileRes.getPath());
if(file.exists()) {
if(fileRes.getStatus()==0){
//文件上传部分
map = new HashMap<>();
map.put("flag", "2");
map.put("fileId", fileRes.getUuid());
map.put("date",DataUntil.StrDta());
map.put("status", true);
}else if(fileRes.getStatus()==1){
//文件上传成功
map = new HashMap<>();
map.put("flag", "3");
map.put("path", fileRes.getPath());
map.put("fileId", fileRes.getUuid());
map.put("date",DataUntil.StrDta());
map.put("status", true);
map.put("size",fileRes.getSize());
map.put("timelength",fileRes.getTimeLength());
map.put("type",fileRes.getSuffix());
}
}else {
//重新上传
//删除表
String uuid = UUID.randomUUID().toString();
map = new HashMap<>();
map.put("flag", "1");
map.put("fileId", uuid);
map.put("date", DataUntil.StrDta());
map.put("status", true);
}
}
return map;
}
/**
* 创建文件路径
* @param string
* @param date
*
* */
public static String CreatePath(String date, String uuid) {
//生成上传文件的路径信息,按天生成
String saveDirectory = ContantFinalUtil.BASE_PATH+
ContantFinalUtil.VIDEO_PATH + File.separator + date + File.separator + uuid;
//验证路径是否存在,不存在则创建目录
File path = new File(saveDirectory);
if (!path.exists()) {
path.mkdirs();
}
return saveDirectory;
}
/**
* 创建文件夹路径
* @param string
* @param date
*
* */
public static String Createfolder(String date) {
//生成上传文件的路径信息,按天生成
String saveDirectory = ContantFinalUtil.BASE_PATH+
ContantFinalUtil.VIDEO_PATH + File.separator + date;
//验证路径是否存在,不存在则创建目录
File path = new File(saveDirectory);
if (!path.exists()) {
path.mkdirs();
}
return saveDirectory;
}
public static Map upload(MultipartFile multipartFile, String saveDirectory, String Pname) {
Map map = null;
try {
multipartFile.transferTo(new File(saveDirectory,Pname));
} catch (IllegalStateException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
//写入 比较与前台传过来的MD5是否一致
return map;
}
公司前端框架是layui,所以改成了这个样子,大家使用的时候自己修改吧。
下面说说这里的几个要点以及与完善原博主的地方:
(前端部分)
1、md5.js ,你只需要下载一个 MD5.js即可,(md5.js和jQuery.md5.js是部分相同的两种东西,我当时以为功能会相同,然后前台报错了,特此记录一下)
2、添加了一个进度条,直接用 当前成功分块/总分块,求出的parcent,layui每500ms轮询一次请求这个值,自动生成的进度条。一个G,5M一块, 一共241块,所以这个进度条看着也可以,不知道大家有没有更好的进度条方式哪?请指点。谢谢。
(后端部分)
1、我这里添加了数据操作,原博主是个大神不屑这些小地方。我补全了,留个记录方便自己。
2、添加了断点续传的功能,理由同上。
3、抽取了一个FileOpera为了断点上传的方便。
4、在数据库种找到记录之后,有添加了一下验证本地。因为我们这边总会有不严谨的删除,很神奇。
最后说说需要改进的地方,以后有时间进行改进:
1、后台接受form提交,我直接使用的 getParamerter,应该使用bean自动的。
2、添加一个线程,可以一边上传,一边自动拼接,最后一次分块上传就会快很多。
3、加一个分块文件七天删除的功能,每上传一次文件,服务器就会存了两份文件。你要是想用断点续传,就不能立刻删除这些分块文件。但是时间长了,就会对服务器造成压力。所以应该定时删除。
第一次写博客,希望大家支持一下,哪里不足的也希望各位指点一下。谢谢。