有时用户上传下载文件需要历时数小时,万一线路中断,不具备断点续传的方式就只能从头重传,断点续传方式允许用户从上传下载断线的地方继续传送,这样大大减少了用户的烦恼。
分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。分片上传不仅可以避免因网络环境不好导致的一直需要从文件起始位置还是上传的问题,还能使用多线程对不同分块数据进行并发发送,提高发送效率,降低发送时间。
分片上传主要适用于以下几种场景:
网络环境不好:当出现上传失败的时候,可以对失败的Part进行独立的重试,而不需要重新上传其他的Part。
断点续传:中途暂停之后,可以从上次上传完成的Part的位置继续上传。
加速上传:要上传到OSS的本地文件很大的时候,可以并行上传多个Part以加快上传。
流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见。
文件较大:一般文件比较大时,默认情况下一般都会采用分片上传。
分片上传的整个流程大致如下:
将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;
初始化一个分片上传任务,返回本次分片上传唯一标识;
按照一定的策略(串行或并行)发送各个分片数据块;
发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。
前台展示
前台代码:
上传之前检测是否存在已上传的分片文件,已存在分片则继续上传,否则重新开始上传。上传完成后执行合并分片操作,合并完成后删除之前的分片文件。
}
后台controller:采用MD5加密方式,生成上传文件的路径,每次上传之前都检测是否存在上传的分片。
//获取分片
@GetMapping("/testing/{fileName}/{fileSlicSize}/{fileSize}")
@ResponseBody
public Result testing(@PathVariable String fileName, @PathVariable long fileSlicSize, @PathVariable long fileSize ) throws Exception {
String dir = FileSliceUploadUtil.fileNameMd5Dir(fileName,fileSize);
String absoluteFilePathAndCreate = FileTool.getAbsoluteFilePathAndCreate(FileSliceUploadUtil.uploadslicedir)+File.separator+dir;
File file = new File(absoluteFilePathAndCreate);
if (file.exists()) {
//从小到大文件进行按照序号排序,和判断分片是否损坏
List collect = FileSliceUploadUtil.fileSliceIsbadAndSort(file, fileSlicSize);
//获取最后一个分片
String fileSliceName = collect.get(collect.size() - 1);
fileSliceName = new File(fileSliceName).getName();
int code = FileSliceUploadUtil.fileId(fileSliceName);
//服务器的分片总大小必须小于或者等于文件的总大小
if ((code*fileSlicSize)<=fileSize) {
Result result = new Result();
HashMap map = new HashMap<>();
map.put("code", code);
map.put("fileSliceName", fileSliceName);
result.setResult(map);
return result;
}else {
//分片异常 ,删除全部分片文件,从新上传
FileTool.delAllFile(absoluteFilePathAndCreate);
return Result.error("error");
}
}
//不存在
return Result.error("error");
}
@PostMapping(value = "/uploads")
@ResponseBody
public Result uploads(HttpServletRequest request) {
String fileSliceName = request.getParameter("fileSliceName");
long fileSize = Long.parseLong(request.getParameter("fileSize")); //文件大小
String dir = FileSliceUploadUtil.fileSliceMd5Dir(fileSliceName,fileSize);
String absoluteFilePathAndCreate = FileTool.getAbsoluteFilePathAndCreate(FileSliceUploadUtil.uploadslicedir+dir);
FileTool.fileUpload(absoluteFilePathAndCreate,fileSliceName,request);
int i = FileSliceUploadUtil.fileId(fileSliceName); //返回上传成功的文件id,用于前端计算进度
Result result=new Result();
result.setResult(i);
return result;
}
/**
*
* @param fileSlicNamee
* @param fileSlicSize 单个分片大小
* @param fileSize 文件总大小
* @return
* @throws Exception
*/
// 合并分片
@GetMapping(value = "/merge-file-slice/{fileSlicNamee}/{fileSlicSize}/{fileSize}")
@ResponseBody
public Result mergeFileSlice(@PathVariable String fileSlicNamee,@PathVariable long fileSlicSize,@PathVariable long fileSize ) throws Exception {
int l =(int) Math.ceil((double) fileSize / fileSlicSize); //有多少个分片
String dir = FileSliceUploadUtil.fileSliceMd5Dir(fileSlicNamee,fileSize); //分片所在的目录
String absoluteFilePathAndCreate = FileTool.getAbsoluteFilePathAndCreate(FileSliceUploadUtil.uploadslicedir+dir);
File file=new File(absoluteFilePathAndCreate);
if (file.exists()){
List filesAll = FileTool.listFiles(file.getAbsolutePath());
//阻塞循环判断是否还在上传 ,解决前端进行ajax异步上传的问题
int beforeSize=filesAll.size();
while (true){
Thread.sleep(1000);
//之前分片数量和现在分片数据只差,如果大于1那么就在上传,那么继续
filesAll = FileTool.listFiles(file.getAbsolutePath());
if (filesAll.size()-beforeSize>=1){
beforeSize=filesAll.size();
//继续检测
continue;
}
//如果是之前分片和现在的分片相等的,那么在阻塞2秒后检测是否发生变化,如果还没变化那么上传全部完成,可以进行合并了
//当然这不是绝对的,只能解决网络短暂的波动,因为有可能发生断网很长时间,网络恢复后文件恢复上传, 这个问题是避免不了的,所以我们在下面的代码进行数量的效验
// 因为我们不可能一直等着他网好,所以如果1~3秒内没有上传新的内容,那么我们默认判定上传完毕
if (beforeSize==filesAll.size()){
Thread.sleep(2000);
filesAll = FileTool.listFiles(file.getAbsolutePath());
if (beforeSize==filesAll.size()){
break;
}
}
}
//分片数量效验
if (filesAll.size()!=l){
//分片缺少 ,删除全部分片文件,从新上传
FileTool.delAllFile(absoluteFilePathAndCreate);
return Result.error("error");
}
//获取实际的文件名称,组装路径
String realFileName = FileSliceUploadUtil.realFileName(fileSlicNamee);
File uploaddir = new File(FileSliceUploadUtil.uploaddir);
if(!uploaddir.exists()||!uploaddir.isDirectory()){
uploaddir.mkdirs();
}
String realFileNamePath = FileTool.getAbsoluteFileAndCreate(FileSliceUploadUtil.uploaddir+ realFileName);
//从小到大文件进行按照序号排序 ,和检查分片文件是否有问题
List collect = FileSliceUploadUtil.fileSliceIsbadAndSort(file, fileSlicSize);
File outputFile = new File(realFileNamePath);
//创建文件
outputFile.createNewFile();
//输出流
FileChannel outChnnel = new FileOutputStream(outputFile).getChannel();
//合并
FileChannel inChannel;
for (String filePath : collect) {
File fileSclice = new File(filePath);
inChannel = new FileInputStream(fileSclice).getChannel();
inChannel.transferTo(0, inChannel.size(), outChnnel);
inChannel.close();
//删除分片
fileSclice.delete();
}
outChnnel.close();
}else {
//没有这个分片相关的的目录
return Result.error("error");
}
return Result.ok("ok");
}
工具类FileSliceUploadUtil
public static final String identification="-slice-";
public static final String uploadslicedir="F:\\home\\sliceupload\\uploads"+File.separator+"slice"+File.separator;//分片目录
public static final String uploaddir="F:\\home\\sliceupload\\uploads"+File.separator+"real"+File.separator;//实际文件目录
public static void main(String[] args) {
System.out.println(HashUtil.md5("123123213"));
System.out.println(HashUtil.md5("123123213"));
}
//获取分片文件的目录
public static String fileSliceMd5Dir(String fileSliceName,long fileSize){
int i = fileSliceName.indexOf(identification) ;
String substring = fileSliceName.substring(0, i);
String dir = HashUtil.md5(substring+fileSize);
return dir;
}
//通过文件名称获取文件目录
public static String fileNameMd5Dir(String fileName,long fileSize){
return HashUtil.md5(fileName+fileSize);
}
//获取分片的实际文件名
public static String realFileName(String fileSliceName){
int i = fileSliceName.indexOf(identification) ;
String substring = fileSliceName.substring(0, i);
return substring;
}
//获取文件序号
public static int fileId(String fileSliceName){
int i = fileSliceName.indexOf(identification)+identification.length() ;
String fileId = fileSliceName.substring(i);
return Integer.parseInt(fileId);
}
//判断是否损坏
public static List fileSliceIsbadAndSort(File file, long fileSlicSize) throws Exception {
String absolutePath = file.getAbsolutePath();
List filesAll = FileTool.listFiles(absolutePath);
if (filesAll.size()<1){
//分片缺少,删除全部分片文件 ,从新上传
FileTool.delAllFile(absolutePath);
throw new Exception("分片损坏");
}
//从小到大文件进行按照序号排序
List collect = filesAll.stream().sorted((a, b) -> fileId(a) - fileId(b)).collect(Collectors.toList());
//判断文件是否损坏,将文件排序后,进行前后序号相差大于1那么就代表少分片了
for (int i = 0; i < collect.size()-1; i++) {
//检测分片的连续度
if (fileId(collect.get(i)) - fileId(collect.get(i+1))!=-1) {
//分片损坏 删除全部分片文件 ,从新上传
FileTool.delAllFile(absolutePath);
throw new Exception("分片损坏");
}
//检测分片的完整度
if (new File(collect.get(i)).length()!=fileSlicSize) {
//分片损坏 删除全部分片文件 ,从新上传
FileTool.delAllFile(absolutePath);
throw new Exception("分片损坏");
}
}
return collect;
}
工具类FileTool
/**
* 创建文件夹
* @param path
* @return
*/
public static String getAbsoluteFilePathAndCreate(String path){
File dir = new File(path);
if(!dir.exists()||!dir.isDirectory()){
dir.mkdirs();
}
return path;
}
/**
* 创建文件
* @param path
* @return
*/
public static String getAbsoluteFileAndCreate(String path){
try {
File file = new File(path);
if(!file.exists()){
file.createNewFile();
}
}catch (Exception e){
e.printStackTrace();
}
return path;
}
/**
* 获取文件列表
* @param path
* @return
*/
public static List listFiles(String path){
File file = new File(path);
List filesAll = new ArrayList<>();
File[] listFiles = file.listFiles();
for(File fileChild: listFiles){
filesAll.add(fileChild.getAbsolutePath());
}
return filesAll;
}
/**
*
* @param directory 文件上传的目录
* @param request
*/
@SneakyThrows
public static void fileUpload(String directory, String fileName, HttpServletRequest request) {
//上传的位置 这句代码就是将文件上传到当前服务器的根目录下uploads文件夹里添加
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
MultiValueMap multiFileMap = multipartRequest.getMultiFileMap();
//拿到所有上传的文件对象
for (Map.Entry stringMultipartFileEntry : multiFileMap.toSingleValueMap().entrySet()) {
MultipartFile uploadFile = stringMultipartFileEntry.getValue();
//判断文件上传表单是否是空
boolean empty = uploadFile.isEmpty();
//如果是空 的file表单那么 跳过
if (!empty) {
//完成文件上传
uploadFile.transferTo(new File(directory, fileName));
} else {
throw new Exception("上传的文件是空的");
}
}
}
/**
* 删除文件夹里面的所有文件
*
* @param path 文件夹路径
*/
public static void delAllFile(String path) {
File file = new File(path);
if (!file.exists()) {
return;
}
if (!file.isDirectory()) {
return;
}
String[] childFiles = file.list();
File temp = null;
for (int i = 0; i < childFiles.length; i++) {
//File.separator与系统有关的默认名称分隔符
//在UNIX系统上,此字段的值为'/';在Microsoft Windows系统上,它为 '\'。
if (path.endsWith(File.separator)) {
temp = new File(path + childFiles[i]);
} else {
temp = new File(path + File.separator + childFiles[i]);
}
if (temp.isFile()) {
temp.delete();
}
if (temp.isDirectory()) {
delAllFile(path + "/" + childFiles[i]);// 先删除文件夹里面的文件
delFolder(path + "/" + childFiles[i]);// 再删除空文件夹
}
}
}
工具类HashUtil
public static String md5(String s) {
char hexChars[] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f' };
try {
byte[] bytes = s.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (Exception e) {
return null;
}
}