文件分片上传_异步
前言
继续学习文件大文件上传,本次采用html5+jquery实现对文件分片
后台需新加接口
一、前端页面修改
1) 采用h5 file.slice(start,end)实现对大文件分片.
2) 构建form表单准备需要的入参.
入参需要构建file,name,chunks,chunk,guid
3) 使用ajax发送异步请求.
后台接口:/fileUpload/fileSyn
note:
finished(data)回调函数,需要根后台定义好返回数据类型.此处文件分片,合成完成后,前端会回显上传成功后的fileId(guid).
4) 前端代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="/webuploader/bootstrap.css">
<link rel="stylesheet" type="text/css" href="/webuploader/webuploader.css">
<link type="text/css" href="/webuploader/bootstrap-theme.css">
<script type="text/javascript" src="js/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="webuploader/webuploader.min.js"></script>
<script type="text/javascript" src="webuploader/bootstrap.min.js"></script>
<style>
.rg_layout{
width: 500px;
height: 120px;
border: 8px solid #EEEEEE;
background-color: #d9edf7;
/*让div水平居中*/
margin: auto;
padding: auto;
}
.rg_left{
/*border: 1px solid red;*/
float: left;
margin: 15px;
}
.rg_right{
/*border: 1px solid red;*/
float: right;
margin: 15px;
}
</style>
<script >
var page = {
init:function () {
$("#ctlBtn_syn").click($.proxy(this.upload,this));
},
upload:function () {
var guid = Math.random().toString().substring(2,8)
var file = $("#file_syn")[0].files[0]; //文件对象
if (file){
$("#ctlBtn_syn").attr({"disabled":"disabled"});
$("#output").text("文件正在上传...");
var name = file.name, //文件名
size = file.size; //总大小
var shardSize = 20 * 1024 * 1024, //以25MB为一个分片
shardCount = Math.ceil(size / shardSize); //总片数
for(var i = 0;i < shardCount;++i){
//计算每一片的起始与结束位置
var start = i * shardSize,
end = Math.min(size, start + shardSize);
//构造一个表单,FormData是HTML5新增的
var form = new FormData();
form.append("file", file.slice(start,end)); //slice方法用于切出文件的一部分
form.append("name", name);
form.append("chunks", shardCount); //总片数
form.append("chunk", i); //当前是第几片
form.append("guid", guid); //当前是第几片
//Ajax提交
$.ajax({
url: "../fileUpload/fileSyn",
type: "POST",
data: form,
async: true, //异步
processData: false, //jquery不要对form进行处理
contentType: false, //指定为false才能形成正确的Content-Type
success: function(data){
//成功后的事件
finished(data)
}
});
}
}else {
alert("请选择要上传的文件")
}
}
}
$(function () {
// 利用h5实现分片上传, ajax实现异步
page.init();
var guid
var uploader = WebUploader.create({
// swf文件路径
swf : 'webuploader/Uploader.swf',
// 文件接收服务端。
// server : 'http://localhost:8080/fileUploaderServlet',
server : "/fileUpload/file",
// 选择文件的按钮。可选。
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick : {id:'#picker'},
chunked: true, //分片处理
chunkSize: 50 * 1024 * 1024, //每片10M
threads:1,//上传并发数。允许同时最大上传进程数。
formData:{'guid':guid},
// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
resize : false
});
// 当有文件被添加进队列的时候
uploader.on('fileQueued', function(file) {
$("#thelist").empty();
$("#thelist").append('<h4 id="info" class="info">' + file.name + '</h4>');
});
// 当文件开始上传的时候
uploader.on('uploadStart', function() {
guid = uploader.options.formData.guid = Math.random().toString().substring(2,8);
$("#info").text("文件正在上传....")
});
//
uploader.on('uploadFinished',function () {
$("#info").empty();
$("#thelist").append('<h4 id="info" class="info">' + '文件上传完成' + '</h4>');
});
$("#ctlBtn").on ('click',function () {
if ($(this).hasClass('disabled')){
alert("disabled");
return false;
}
uploader.upload();
})
});
function finished(data) {
if (data.code=="1"&& data.msg=="success"){
$("#ctlBtn_syn").removeAttr("disabled");
$("#output").text("文件上传完成;fileId="+data.data);
var file = document.getElementById('file_syn');
file.value = '';
}
}
</script>
</head>
<body>
<div id="uploader" class="rg_layout">
<div class="rg_left">
<div id="picker">选择文件</div>
<div id="thelist" class="uploader-list"></div>
</div>
<div class="rg_right">
<button id="ctlBtn" class="btn btn-default">开始上传</button>
</div>
</div>
<div id="uploader_syn" class="rg_layout">
<div class="rg_left">
<input type="file" id="file_syn">
<br>
<span id="output"></span>
</div>
<div class="rg_right">
<button id="ctlBtn_syn">开始上传</button>
</div>
</div>
</body>
</html>
5) UI
二、后台新增接口
1) 引入commons-io 依赖
<!-- 引入commons-io 依赖 -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
</dependency>
2) 新增接口
URL:/fileUpload/fileSyn
Method:POST
FormData: file,name,chunks,chunk,guid
3) 业务层实现
和同步上传代码几乎差不多,差异主要是写入数据时使用了IOUtils.copy(inStream,outStream); 简写了代码
前端使用ajax发送异步请求,后台会引入并发问题,以及什么时候开始合并文件
解决方案:可以采用缓存,来统计分片完成次数, 当统计次数等于总片数即可开始合并
创建:
private static Map<String,Integer> cache = new ConcurrentHashMap<>()和
private static synchronized boolean checkChunk (String key,int chunks)方法
检查:checkChunk为true开始合并文件
4) 业务层代码
package com.service.management.service;
import com.service.management.base.Result;
import com.service.management.utils.IoUtils;
import com.service.management.vo.BreakPointFile;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class FileUploadService {
private static final Logger LOGGER = LoggerFactory.getLogger(FileUploadService.class);
public Result uploadFile(BreakPointFile breakPointFile) {
Result result = Result.success(breakPointFile.getName());
String fileId = breakPointFile.getGuid();
Integer chunk =
breakPointFile.getChunk() == null ? 0 : breakPointFile.getChunk();
Integer chunks =
breakPointFile.getChunks() == null ? 1 : breakPointFile.getChunks();
File fileDir = null;
try {
fileDir = new File("F:/tempFile/" + fileId);
if (!fileDir.isDirectory()) {
fileDir.mkdirs();
}
File file = new File(fileDir, chunk + "");
if (!file.exists()) {
file.createNewFile();
}
outPutSliceFile(file,breakPointFile);
LOGGER.info("分片上传({}/{})" ,chunk+1,chunks);
} catch (Exception e) {
e.printStackTrace();
Result.error(e);
}
if (chunks.intValue() == chunk + 1) {
LOGGER.info("分片完成!");
LOGGER.info("开始合并...");
InputStream in = null;
OutputStream out = null;
try {
String path = fileDir.getAbsolutePath() + "/" + breakPointFile.getName();
File[] files = fileDir.listFiles();
List<File> fileList = Arrays.asList(files);
List<File> list = fileList.stream()
.sorted(Comparator.comparing(this::getSortName))
.collect(Collectors.toList());
out = new FileOutputStream(path);
for (File file : list) {
in = new FileInputStream(file);
int len = 0;
byte[] bt = new byte[1024];
while (-1 != (len = in.read(bt))) {
out.write(bt, 0, len);
}
in.close();
}
// 删除上传的分片文件
deletTempFIles(fileList);
} catch (Exception e) {
e.printStackTrace();
Result.error(e);
} finally {
IoUtils.Close(in);
IoUtils.Close(out);
}
LOGGER.info("合并完成!");
LOGGER.info("文件上传完成! fieId = " + fileId);
}
return result;
}
private void deletTempFIles(List<File> fileList) {
Iterator<File> iterator = fileList.iterator();
iterator.forEachRemaining(file -> {
file.delete();
});
}
private void outPutSliceFile(File file, BreakPointFile breakPointFile) throws IOException {
OutputStream outSliceFile = new FileOutputStream(file);
byte[] bytes = breakPointFile.getFile().getBytes(); //保存文件
outSliceFile.write(bytes);
IoUtils.Close(outSliceFile);
}
private int getSortName(File file) {
int i = Integer.parseInt(file.getName());
return i;
}
private static Map<String,Integer> cache = new ConcurrentHashMap<>();
private static synchronized boolean checkChunk (String key,int chunks){
Integer counter = cache.get(key);
if (counter == null){
counter = 0;
}
counter++;
cache.put(key,counter);
return chunks == counter.intValue();
}
public Result uploadFileSyn(BreakPointFile breakPointFile) {
Result result = Result.error(breakPointFile.getName());
String fileId = breakPointFile.getGuid();
Integer chunk =
breakPointFile.getChunk() == null ? 0 : breakPointFile.getChunk();
Integer chunks =
breakPointFile.getChunks() == null ? 1 : breakPointFile.getChunks();
File fileDir = null;
try{
fileDir = new File("F:/tempFileSyn/" + fileId);
if (!fileDir.isDirectory()) {
fileDir.mkdirs();
}
File file = new File(fileDir, chunk + "");
if (!file.exists()) {
file.createNewFile();
}
InputStream inStream = breakPointFile.getFile().getInputStream();
OutputStream outStream = new FileOutputStream(file);
IOUtils.copy(inStream,outStream);
IoUtils.Close(inStream);
IoUtils.Close(outStream);
LOGGER.info("分片上传({}/{})" ,chunk+1,chunks);
if (checkChunk(fileId,chunks)){
LOGGER.info("分片完成!");
LOGGER.info("开始合并...");
mergeFile(breakPointFile, fileId, fileDir);
result = Result.success(breakPointFile.getGuid());
}
}catch (Exception e){
e.printStackTrace();
}
return result;
}
private void mergeFile(BreakPointFile breakPointFile, String fileId, File fileDir) {
InputStream in = null;
OutputStream out = null;
try {
String path = fileDir.getAbsolutePath() + "/" + breakPointFile.getName();
File[] files = fileDir.listFiles();
List<File> fileList = Arrays.asList(files);
List<File> list = fileList.stream()
.sorted(Comparator.comparing(this::getSortName))
.collect(Collectors.toList());
out = new FileOutputStream(path);
for (File file1 : list) {
in = new FileInputStream(file1);
IOUtils.copy(in,out);
in.close();
}
// 删除上传的分片文件
deletTempFIles(fileList);
} catch (Exception e) {
e.printStackTrace();
Result.error(e);
} finally {
IoUtils.Close(in);
IoUtils.Close(out);
}
LOGGER.info("合并完成!");
LOGGER.info("文件上传完成! fieId = " + fileId);
}
}
总结
本次完成大文件分片异步上传 .
前端技术:JQ + H5 + ajax
注
int类型 == Integer类型比较时,一定一定要使用Integer.intValue()进行比较
注意常量池
感谢您看完这篇文章,本人也在学习和摸索中,如有问题,欢迎指正和交流