参考链接: https://www.jianshu.com/p/aa44eb96c7b6
参考链接: https://www.jianshu.com/p/c449dec43099
以下仅作个人代码记录,若有不正确之处,希望大佬指出。
实现方式最近浏览的看到以下3种,自己总结了下分别做了demo放到了git中。下载即可运行
本文基于第二种方式实现,实现思路如下:
测试上传1.2G文件 大概用时53秒 后端还可以在优化 我就没做了
1.前端处理逻辑
首先读取文件MD5检测后台数据库中 是否存在
若已完成 则提示秒传 并做相应处理
若未上传或未完成 则 进行切片
向后台发送请求check or upload
若 check 未上传 则执行upload 否则 跳过本次 执行下次
2.后端主要处理逻辑
1.将文件分片保存 并检查是否==文件总分片
若等于则 合并文件 合并完成 删除分片文件
现在贴出主要代码逻辑 ,完整请看git example1 里
前端:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>实现方案1title>
head>
<body>
<input type="file" id="file" />
<button id="upload">上传button>
<span id="output" style="font-size:12px">等待span>
<span id="usetime" style="font-size:12px;margin-left:20px;">span>
<span id="uuid" style="font-size:12px;margin-left:20px;">span>
<br/>
<div>上传百分比:<span id="percentage">span> div>
<br/>
<br/>
<br/>
<span id="param" style="font-size:12px;margin-left:20px;">param==span>
<script src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js">script>
<script src="js/spark-md5.js">script>
<script>
script>
<script>
var chunk=-1; //分片数
var succeed=0; //成功标识
var dataBegin;//开始时间
var dataEnd;//结束时间
var action=false; //false检验分片是否上传过 (默认) true 表示上传文件
const shardSize = 50 * 1024 * 1024; //以50MB为一个分片
var page={
init: function () {
$("#upload").click(function () {
dataBegin=new Date();
var file=$("#file")[0].files[0]; //文件对象
$("#output").text("开始上传中。。。")
CheckIsUpload(file);
})
}
}
$(function () {
page.init();
})
/**
* 检查文件是否上传
*/
function CheckIsUpload(file) {
chunk=-1;
succeed=0; //成功标识
dataBegin;//开始时间
dataEnd;//结束时间
action=false; //false检验分片是否上传过 (默认) true 表示上传文件
//构建一个表单 FoemData Html5新增的
var form=new FormData();
var r=new FileReader();
r.readAsArrayBuffer(file);
$(r).load(function (e) {
var blob= e.target.result;
var fileMd5=SparkMD5.ArrayBuffer.hash(blob);
form.append("md5",fileMd5);
console.log(fileMd5)
//Ajax提交
$.ajax({
url: "example1/isUpload",
type: "POST",
data: form,
async: true,
processData: false,//很重要,告诉jquery不要对form进行处理
contentType: false, //很重要,指定为false才能形成正确的Content-Type
success: function (data) {
handleCheckIsUploadResult(file,fileMd5,data);
},
error: function(XMLHttpRequest, textStatus, errorThrown) {
alert("服务器出错!");
}
})
})
}
/**
* 处理检查是否上传的结果
* @param file 当前文件
* data: fileId date flag
* @param flag 0未上传 1 部分上传 2 妙传逻辑
*/
function handleCheckIsUploadResult(file,fileMd5,data) {
var uuid=data.fileId; //后端返回uuid
let date=data.date;
let flag=data.flag; //上传标记 0 未上传过 1 部分上传 2 妙传逻辑
if (flag=="0"){
//没有上传过文件
upload(file,uuid,fileMd5,date)
} else if (flag=="1"){
//已经上传了部分
upload(file,uuid,fileMd5,date);
} else if (flag=="2"){
alert("文件已经上传过 妙传了")
$("#uuid").append(uuid);
}
}
/**
*
* @param file文件对象
* @param uuid 后端生成的uuid
* @param filemd5 整个文件的md5
* @param date 文件第一个分片上传日期
*/
function upload(file,uuid,filemd5,date) {
let name = file.name; //文件名
let size = file.size; //总大小
let shardCount = Math.ceil(size / shardSize); //总片数
//分片数>分片总数 结束本次方法调用
if (chunk> shardCount) return;
if(!action){
chunk += 1; //只有在检测分片时,i才去加1; 上传文件时无需加1
}
//计算每一片的起始和结束位置
let start=chunk*shardSize;
let end=Math.min(size,start+shardSize);
//构造一个表单,FormData是HTML5新增的
var form = new FormData();
if(!action){
form.append("action", "check"); //检测分片是否上传
$("#param").append("action==check ");
}else{
form.append("action", "upload"); //直接上传分片
form.append("data", file.slice(start,end)); //slice方法用于切出文件的一部分
$("#param").append("action==upload ");
}
form.append("uuid", uuid);
form.append("md5", filemd5);
form.append("date", date);
form.append("name", name);
form.append("size", size);
form.append("total", shardCount); //总片数
form.append("index", chunk+1); //当前是第几片
var index = chunk+1;
$("#param").append("index=="+index+"
");
//按大小切割文件段
var data = file.slice(start, end);
var r = new FileReader();
r.readAsArrayBuffer(data);
$(r).load(function (e) {
var bolb = e.target.result;
var partMd5=SparkMD5.ArrayBuffer.hash(bolb);
form.append("partMd5", partMd5);
//Ajax提交
$.ajax({
url: "example1/upload",
type: "POST",
data:form,
async: true,
processData: false, //很重要,告诉jquery不要对form进行处理
contentType: false, //很重要,指定为false才能形成正确的Content-Type
success: function (data) {
handleUploadResult(file,uuid,filemd5,date,shardCount,data);
},error: function(XMLHttpRequest, textStatus, errorThrown) {
alert("服务器出错!");
}
})
})
}
/**
* 处理上传的结果
* @param file
* @param uuid
* @param filemd5
* @param date
* @param shardCount
* @param data
*/
function handleUploadResult(file,uuid,filemd5,date,shardCount,data) {
var fileuuid = data.fileId;
var flag = data.flag;
//服务器返回该分片是否上传过
if (flag=="2"){
++succeed;
$("#output").text(succeed + " / " + shardCount);
//服务器返回分片是否上传成功
if (succeed == shardCount) {
dataEnd = new Date();
$("#uuid").text("uuid="+fileuuid);
$("#usetime").text("用时:"+((dataEnd.getTime() - dataBegin.getTime())/1000)+"s");
$("#param").append("
" + "上传成功!");
}
}
else{
if (flag=="0"){
//未上传,继续上传
action = true;
}
else if (flag=="1"){
action=false;
++succeed;
$("#output").text(succeed + " / " + shardCount);
$("#percentage").text(succeed/shardCount*100);
}
upload(file, uuid, filemd5, date);
}
}
script>
body>
html>
后端:
package com.lovecyy.file.up.example1.service.impl;
import com.lovecyy.file.up.example1.constants.Constant;
import com.lovecyy.file.up.example1.dao.UploadFileRepository;
import com.lovecyy.file.up.example1.pojo.MultipartFileParam;
import com.lovecyy.file.up.example1.pojo.UploadFile;
import com.lovecyy.file.up.example1.service.UploadFileService;
import com.lovecyy.file.up.example1.utils.FileMd5Util;
import com.lovecyy.file.up.example1.utils.KeyUtil;
import com.lovecyy.file.up.example1.utils.NameUtil;
import org.apache.commons.io.FileUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @author ys
* @topic
* @date 2020/3/10 14:19
*/
@Service
public class UploadFileServiceImpl implements UploadFileService {
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
@Autowired
UploadFileRepository uploadFileRepository;
@Override
public Map<String, Object> findByFileMd5(String md5) {
UploadFile uploadFile = uploadFileRepository.findByFileMd5(md5);
Map<String, Object> map = null;
if (uploadFile == null) {
//没有上传过文件
map = new HashMap<>();
map.put("flag", 0);
map.put("fileId", KeyUtil.genUniqueKey());
map.put("date", simpleDateFormat.format(new Date()));
}else{
//上传过文件 判断文件现在还是否存在
File file = new File(uploadFile.getFilePath());
if (!file.exists()){
//若不存在
map = new HashMap<>();
map.put("flag", 0);
map.put("fileId", uploadFile.getFileId());
map.put("date", simpleDateFormat.format(new Date()));
}
//若文件存在 判断此时是部分上传了 还是已全部上传
else{
int fileStatus = uploadFile.getFileStatus().intValue();
if (fileStatus==1){
//文件只上传了一部分
map = new HashMap<>();
map.put("flag", 1);
map.put("fileId", uploadFile.getFileId());
map.put("date", simpleDateFormat.format(new Date()));
}else if (fileStatus==2){
//文件早已上传完整
map = new HashMap<>();
map.put("flag" , 2);
}
}
}
return map;
}
@Override
public Map<String, Object> realUpload(MultipartFileParam form, MultipartFile multipartFile) throws IOException, Exception {
String action = form.getAction();
String fileId = form.getUuid();
Integer index = Integer.valueOf(form.getIndex());
String partMd5 = form.getPartMd5();
String md5 = form.getMd5();
Integer total = Integer.valueOf(form.getTotal());
String fileName = form.getName();
String size = form.getSize();
String suffix = NameUtil.getExtensionName(fileName);
String saveDirectory = Constant.PATH + File.separator + fileId;
String filePath = saveDirectory + File.separator + fileId + "." + suffix;
//验证路径是否存在,不存在则创建目录
File path = new File(saveDirectory);
if (!path.exists()) {
path.mkdirs();
}
//文件分片位置
File file = new File(saveDirectory, fileId + "_" + index);
//根据action不同执行不同操作. check:校验分片是否上传过; upload:直接上传分片
Map<String, Object> map = null;
if ("check".equals(action)){
String md5Str = FileMd5Util.getFileMD5(file);
if (md5Str != null && md5Str.length() == 31) {
System.out.println("check length =" + partMd5.length() + " md5Str length" + md5Str.length() + " " + partMd5 + " " + md5Str);
md5Str = "0" + md5Str;
}
if (md5Str != null && md5Str.equals(partMd5)) {
//分片已上传过
map = new HashMap<>();
map.put("flag", "1");
map.put("fileId", fileId);
if(!index .equals(total))
return map;
} else {
//分片未上传
map = new HashMap<>();
map.put("flag", "0");
map.put("fileId", fileId);
return map;
}
}
else if("upload".equals(action)){
//分片上传过程中出错,有残余时需删除分块后,重新上传
if (file.exists()) {
file.delete();
}
multipartFile.transferTo(new File(saveDirectory, fileId + "_" + index));
map = new HashMap<>();
map.put("flag", "1");
map.put("fileId", fileId);
if(!index .equals(total) )
return map;
}
if (path.isDirectory()){
File[] fileArray = path.listFiles();
if (fileArray!=null){
if (fileArray.length == total){
//分块全部上传完毕,合并
File newFile = new File(saveDirectory, fileId + "." + suffix);
FileOutputStream outputStream = new FileOutputStream(newFile, true);//文件追加写入
for (int i = 0; i < fileArray.length; i++) {
File tmpFile = new File(saveDirectory, fileId + "_" + (i + 1));
FileUtils.copyFile(tmpFile,outputStream);
//应该放在循环结束删除 可以避免 因为服务器突然中断 导致文件合并失败 下次也无法再次合并
tmpFile.delete();
}
outputStream.close();
//修改FileRes记录为上传成功
UploadFile uploadFile = new UploadFile();
uploadFile.setFileId(fileId);
uploadFile.setFileStatus(2);
uploadFile.setFileName(fileName);
uploadFile.setFileMd5(md5);
uploadFile.setFileSuffix(suffix);
uploadFile.setFilePath(filePath);
uploadFile.setFileSize(size);
uploadFileRepository.save(uploadFile);
map=new HashMap<>();
map.put("fileId", fileId);
map.put("flag", "2");
return map;
}else if (index==1){
//文件第一个分片上传时记录到数据库
UploadFile uploadFile = new UploadFile();
uploadFile.setFileMd5(md5);
String name = NameUtil.getFileNameNoEx(fileName);
if (name.length() > 32) {
name = name.substring(0, 32);
}
uploadFile.setFileName(name);
uploadFile.setFileSuffix(suffix);
uploadFile.setFileId(fileId);
uploadFile.setFilePath(filePath);
uploadFile.setFileSize(size);
uploadFile.setFileStatus(1);
uploadFileRepository.save(uploadFile);
}
}
}
return map;
}
}
前台:
<html lang="en">
<head>
<meta charset="UTF-8">
<title>操作视频title>
head>
<body>
<div style="text-align: center; margin-top: 50px; ">
<h2>操作视频h2>
后台:
@GetMapping("download/{fileName}")
@ResponseBody
public void videoStart(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response){
String filePath="E:\\idea\\base\\file\\1583847208056206066\\";
String realPath = filePath + fileName;
chunkDown(realPath,request,response);
}
/*******************************************************/
/**
* 若要实现文件限速下载 只需要后台向前台返回流的时候 睡眠一下就好
*/
/*******************************************************/
/**
* 文件分片下载
* @param filePath
* @param request
* @param response
*/
public void chunkDown(String filePath, HttpServletRequest request, HttpServletResponse response){
String range = request.getHeader("Range");
log.info("current request rang:" + range);
File file = new File(filePath);
//开始下载位置
long startByte = 0;
//结束下载位置
long endByte = file.length() - 1;
log.info("文件开始位置:{},文件结束位置:{},文件总长度:{}", startByte, endByte, file.length());
if (range!=null && range.contains("bytes=")&&range.contains("-")){
range = range.substring(range.lastIndexOf("=") + 1).trim();
String[] ranges = range.split("-");
try{
//判断range的类型
if (ranges.length == 1) {
//类型一:bytes=-2343
if (range.startsWith("-")) {
endByte = Long.parseLong(ranges[0]);
}
//类型二:bytes=2343-
else if (range.endsWith("-")) {
startByte = Long.parseLong(ranges[0]);
}
} else if (ranges.length == 2) {
//类型三:bytes=22-2343
startByte = Long.parseLong(ranges[0]);
endByte = Long.parseLong(ranges[1]);
}
}catch (NumberFormatException e){
startByte=0;
endByte=file.length()-1;
log.error("Range Occur Error, Message:{}",e.getLocalizedMessage());
}
//要下载的长度
long contentLength = endByte - startByte + 1;
//文件名
String fileName = file.getName();
//文件类型
String contentType = request.getServletContext().getMimeType(fileName);
解决下载文件时文件名乱码问题
byte[] fileNameBytes = fileName.getBytes(StandardCharsets.UTF_8);
fileName = new String(fileNameBytes, 0, fileNameBytes.length, StandardCharsets.ISO_8859_1);
//各种响应头设置
//支持断点续传,获取部分字节内容:
response.setHeader("Accept-Ranges", "bytes");
//http状态码要为206:表示获取部分内容
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.setContentType(contentType);
response.setHeader("Content-Type", contentType);
//inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名
response.setHeader("Content-Disposition", "inline;filename=" + fileName);
response.setHeader("Content-Length", String.valueOf(contentLength));
// Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小]
response.setHeader("Content-Range", "bytes " + startByte + "-" + endByte + "/" + file.length());
BufferedOutputStream outputStream = null;
RandomAccessFile randomAccessFile = null;
//已传送数据大小
long transmitted = 0;
try {
randomAccessFile = new RandomAccessFile(file, "r");
outputStream = new BufferedOutputStream(response.getOutputStream());
byte[] buff = new byte[4096];
int len = 0;
randomAccessFile.seek(startByte);
//坑爹地方四:判断是否到了最后不足4096(buff的length)个byte这个逻辑((transmitted + len) <= contentLength)要放前面!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
//不然会会先读取randomAccessFile,造成后面读取位置出错,找了一天才发现问题所在
//此处的原作者意思逻辑就是 (len = randomAccessFile.read(buff)) 每次读取4096个字节 eg 文件剩余2000 读4096 意味着 有2096
//是空的 那么前端解析的时候就会出错 所以此处作者加了(transmitted + len) <= contentLength
//条件判断
while ((transmitted + len) <= contentLength && (len = randomAccessFile.read(buff)) != -1) {
outputStream.write(buff, 0, len);
transmitted += len;
}
//处理不足buff.length部分
if (transmitted < contentLength) {
len = randomAccessFile.read(buff, 0, (int) (contentLength - transmitted));
outputStream.write(buff, 0, len);
transmitted += len;
}
outputStream.flush();
response.flushBuffer();
randomAccessFile.close();
log.info("下载完毕:" + startByte + "-" + endByte + ":" + transmitted);
} catch (ClientAbortException e) {
log.warn("用户停止下载:" + startByte + "-" + endByte + ":" + transmitted);
//捕获此异常表示拥护停止下载
} catch (IOException e) {
e.printStackTrace();
log.error("用户下载IO异常,Message:{}", e.getLocalizedMessage());
}finally {
try {
if (randomAccessFile != null) {
randomAccessFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}///end try
}
}
未做出具体业务实现 主要思路是 向前端输出流的时候 Sleep实现
Demo地址:https://github.com/xiaoashuo/base/tree/master/hello-max-file-up