最近项目需要做上传功能,网上搜索了一下各种的上传插件,最后选择了stream插件,stream支持暂停上传、进度展示、批量上传等核心功能,样式也可以自定义,可以集成bootstrap或layui等框架,比较符合预期。在demo中,我也集成了图片缩略图的生成,视频缩略图的截取等功能,有兴趣的可以看一下
Stream插件:http://twinkling.cn/
首先围观一下
是不是感觉不错,至少我认为是可以的,因为项目本身用的是layui,Stream官网的自定义样式例子用的是bootstrap,我也懒得改了,直接又把bootstrap加入到项目中,有兴趣有时间的同学可以用layui的样式改造一下。
首先说,该控件虽然可以选择多文件同时上传,但文件之间的上传还是串行的,即只允许同时上传一个文件,下面贴出核心的代码,完整的例子详情请在demo中查看
前端代码:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>上传插件集成</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="format-detection" content="telephone=no">
<link rel="stylesheet" href="/layui-2.3.0/css/layui.css" media="all" />
<link href="/streamUpload/css/stream-v1.css" rel="stylesheet" type="text/css">
<link href="/bootstrap-3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="childrenBody" style="width: 95%">
<div class="layui-upload" style="margin-left: 15px;margin-top: 15px;width: 100%">
<button type="button" class="layui-btn layui-btn-normal margin-left" id="uploadFileList">选择多文件</button>
<div class="layui-upload-list" style="width: 100%">
<div class="container" style="width: 100%;min-width: 550px;">
<div class="row clearfix">
<div class="col-md-7 column" style="width: 80%;overflow: auto;max-height: 400px;">
<table id="data_table" class="layui-table" lay-skin="line" style="width: 100%;min-width: 500px;">
<thead>
<tr>
<th>文件名</th>
<th style="width: 40%">进度</th>
<th style="min-width: 60px">大小</th>
<th style="min-width: 70px">操作</th>
</tr>
</thead>
<tbody id="bootstrap-stream-container">
</tbody>
</table>
</div>
</div>
</div>
<div class="container" style="width: 100%">
<div class="row clearfix" style="width: 100%">
<div class="col-md-7 column" style="width: 80%">
<table style="margin-top: 10px;width: 100%" id="stream_total_progress_bar">
<tr>
<th style="min-width: 500px;">
<div class="progress">
<div class="progress-bar progress-bar-success" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">
</div>
</div>
</th>
</tr>
<tr>
<th>
<span class="stream_total_size"></span>
<span class="stream_total_percent"></span>
</th>
</tr>
</table>
</div>
</div>
</div>
<div class="margin-left">
<button type="button" class="layui-btn start" id="startUploadFileButton">
<i class="layui-icon"></i>开始上传
</button>
<button type="button" class="layui-btn" id="canleUploadFileButton">
<i class="layui-icon"></i>取消上传
</button>
</div>
</div>
</div>
<script type="text/javascript" src="/layui-2.3.0/layui.js"></script>
<script type="text/javascript" src="/streamUpload/js/stream-v1.js"></script>
<script src="/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/bootstrap-3.3.7/js/bootstrap.min.js"></script>
<script type="text/javascript">
var config = {
enabled: true, /** 是否启用文件选择,默认是true */
customered: true,
multipleFiles: true, /** 是否允许同时选择多个文件,默认是false */
autoRemoveCompleted: false, /** 是否自动移除已经上传完毕的文件,非自定义UI有效(customered:false),默认是false */
autoUploading: false, /** 当选择完文件是否自动上传,默认是true */
fileFieldName: "FileData", /** 相当于指定<input type="file" name="FileData">,默认是FileData */
maxSize: 2147483648, /** 当_t.bStreaming = false 时(也就是Flash上传时),2G就是最大的文件上传大小!所以一般需要 */
simLimit: 200, /** 允许同时选择文件上传的个数(包含已经上传过的) */
//extFilters: [".ppt",".pptx",".doc",".docx",".xls",".xlsx",".pdf"], /** 默认是全部允许,即 [] */
browseFileId : "uploadFileList", /** 文件选择的Dom Id,如果不指定,默认是i_select_files */
browseFileBtn : "", /** 选择文件的按钮内容,非自定义UI有效(customered:false) */
filesQueueId : "i_stream_files_queue", /** 文件上传进度显示框ID,非自定义UI有效(customered:false) */
filesQueueHeight : 450, /** 文件上传进度显示框的高,非自定义UI有效(customered:false),默认450px */
messagerId : "i_stream_message_container", /** 消息框的Id,当没有自定义onXXX函数,系统会显示onXXX的部分提示信息,如果没有i_stream_message_container则不显示 */
uploadURL : "/uploadFile",
tokenURL : "/queryToken",
postVarsPerFile:{
},
onSelect: function(files) {
},
onMaxSizeExceed: function(file) {
$("#i_error_tips > span.text-message").append("文件[name="+file.name+", size="+file.formatSize+"]超过文件大小限制‵"+file.formatLimitSize+"‵,将不会被上传!<br>");
},
onFileCountExceed : function(selected, limit) {
$("#i_error_tips > span.text-message").append("同时最多上传<strong>"+limit+"</strong>个文件,但是已选择<strong>"+selected+"</strong>个<br>");
},
onExtNameMismatch: function(info) {
$("#i_error_tips > span.text-message").append("<strong>"+info.name+"</strong>文件类型不匹配[<strong>"+info.filters.toString() + "</strong>]<br>");
},
onAddTask: function(file) {
var file = '<tr id="' + file.id + '" class="template-upload fade in">' +
'<td><span class="preview">'+file.name+'</span></td>' +
'<td>' +
' <div> <span class="message-text" style="font-size: 13px"></span></div>' +
' <div class="progress progress-striped active" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">' +
' <div class="progress-bar progress-bar-success" title="" style="width: 0%;"></div>' +
' </div>' +
'</td>' +
'<td><p class="size">' + file.formatSize + '</p>' +
'</td>' +
'<td><span class="glyphicon glyphicon-remove" onClick="javascript:_t.cancelOne(\'' + file.id + '\')"></span>' +
'</td></tr>';
$("#bootstrap-stream-container").append(file);
},
onUploadProgress: function(file) {
var $bar = $("#"+file.id).find("div.progress-bar");
$bar.css("width", file.percent + "%");
var $message = $("#"+file.id).find("span.message-text");
$message.text("已上传:" + file.formatLoaded + "/" + file.formatSize + "(" + file.percent + "%" + ") 速 度:" + file.formatSpeed);
var $total = $("#stream_total_progress_bar");
$total.find("div.progress-bar").css("width", file.totalPercent + "%");
$total.find("span.stream_total_size").html(file.formatTotalLoaded + "/" + file.formatTotalSize);
$total.find("span.stream_total_percent").html(file.totalPercent + "%");
},
onStop: function() {
},
onCancel: function(file) {
$("#"+file.id).remove();
var $total = $("#stream_total_progress_bar");
$total.find("div.progress-bar").css("width", file.totalPercent + "%");
$total.find("span.stream_total_size").text(file.formatTotalLoaded + "/" + file.formatTotalSize);
$total.find("span.stream_total_percent").text(file.totalPercent + "%");
//console && console.log("-------------onCancel-------------------End");
},
onCancelAll: function(numbers) {
$("#i_error_tips > span.text-message").append(numbers + " 个文件已被取消上传!!!");
},
onComplete: function(file) {
/** 100% percent */
var $bar = $("#"+file.id).find("div.progress-bar");
$bar.css("width", file.percent + "%");
var $message = $("#"+file.id).find("span.message-text");
$message.text("已上传:" + file.formatLoaded + "/" + file.formatSize + "(" + file.percent + "%" + ")");
/** remove the `cancel` button */
var $cancelBtn = $("#"+file.id).find("td:last > span");
$cancelBtn.remove();
/** modify the total progress bar */
var $total = $("#stream_total_progress_bar");
$total.find("div.progress-bar").css("width", file.totalPercent + "%");
$total.find("span.stream_total_size").text(file.formatTotalLoaded + "/" + file.formatTotalSize);
$total.find("span.stream_total_percent").text(file.totalPercent + "%");
//console && console.log("-------------onComplete-------------------End");
},
onQueueComplete: function(msg) {
$("#startUploadFileButton").html('<i class="layui-icon"></i>开始上传');
$("#startUploadFileButton").removeClass("stop").addClass("start");
$("#returnListPageButton").removeClass("layui-btn-disabled");
$("#returnListPageButton").prop("disabled",false);
},
onUploadError: function(status, msg) {
$("#i_error_tips > span.text-message").append(msg + ", 状态码:" + status);
}
};
var _t = new Stream(config);
function cancelOne(id){
_t.cancelOne(id);
}
$("#startUploadFileButton").click(function(){
if($(this).hasClass("start")){
$(this).html('<i class="layui-icon"></i>暂停上传');
$(this).removeClass("start").addClass("stop");
$("#returnListPageButton").addClass("layui-btn-disabled");
$("#returnListPageButton").prop("disabled",true);
_t.upload();
}else{
$(this).html('<i class="layui-icon"></i>开始上传');
$(this).removeClass("stop").addClass("start");
$("#returnListPageButton").removeClass("layui-btn-disabled");
$("#returnListPageButton").prop("disabled",false);
_t.stop();
}
});
$("#canleUploadFileButton").click(function(){
_t.cancel();
});
</script>
</body>
</html>
java代码:
TokenController
@Controller
public class TokenController {
public static final String FILE_NAME_FIELD = "name";
public static final String FILE_SIZE_FIELD = "size";
public static final String FILE_TYPE = "fileType";
public static final String TOKEN_FIELD = "token";
public static final String SERVER_FIELD = "server";
public static final String SUCCESS = "success";
public static final String MESSAGE = "message";
@RequestMapping(value = "/queryToken")
public void firstdoget(HttpServletRequest req, HttpServletResponse resp) throws IOException {
doOptions(req, resp);
String name = req.getParameter(FILE_NAME_FIELD); // 文件名
String size = req.getParameter(FILE_SIZE_FIELD); // 文件大小
String token = TokenUtil.generateToken(name, size); // 利用文件名和文件大小重新生成编码作为临时文件的名字
PrintWriter writer = resp.getWriter();
JSONObject json = new JSONObject();
try {
json.put(TOKEN_FIELD, token);
json.put(SUCCESS, true);
json.put(MESSAGE, "");
} catch (JSONException e) {
}
writer.write(json.toString());
}
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=utf-8");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Headers", "Content-Range,Content-Type");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
}
}
UploadController
@Controller
public class UploadController {
static final int BUFFER_LENGTH = 10240;
static final String START_FIELD = "start";
public static final String CONTENT_RANGE_HEADER = "content-range";
@Autowired
private ImageAsync imageAsync;
@Autowired
private VideoAsync videoAsync;
@RequestMapping(value = "/uploadFile", method = RequestMethod.GET)
public void uploadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
long start = 0;
boolean success = true;
String message = "";
JSONObject json = new JSONObject();
final String token = request.getParameter(TokenController.TOKEN_FIELD); // 文件名和大小的hashcode编码
final String size = request.getParameter(TokenController.FILE_SIZE_FIELD); // 文件大小
final String fileName = request.getParameter(TokenController.FILE_NAME_FIELD); // 文件名
final PrintWriter writer = response.getWriter();
try {
doOptions(request, response);
// 创建空文件,以及上级文件夹名
File file = IoUtil.getTokenedFile(token);
start = file.length();
if (token.endsWith("_0") && "0".equals(size) && 0 == start)
file.renameTo(IoUtil.getFile(fileName));
} catch (FileNotFoundException e) {
message = "Error: " + e.getMessage();
success = false;
} finally {
try {
if (success)
json.put(START_FIELD, start);
json.put(TokenController.SUCCESS, success);
json.put(TokenController.MESSAGE, message);
} catch (JSONException e) {
}
writer.write(json.toString());
IoUtil.close(writer);
}
}
@RequestMapping(value = "/uploadFile", method = RequestMethod.POST)
public void uploadFileListPost(HttpServletRequest request, HttpServletResponse response)
throws IOException, Exception {
doOptions(request, response);
// 文件名和大小的hashcode编码
final String token = request.getParameter(TokenController.TOKEN_FIELD);
// 文件名
final String fileName = request.getParameter(TokenController.FILE_NAME_FIELD);
Range range = IoUtil.parseRange(request);
OutputStream out = null;
InputStream content = null;
final PrintWriter writer = response.getWriter();
JSONObject json = new JSONObject();
long start = 0;
boolean success = true;
String message = "";
File f = IoUtil.getTokenedFile(token);
try {
if (f.length() != range.getFrom()) {
/** drop this uploaded data */
throw new StreamException(StreamException.ERROR_FILE_RANGE_START);
}
out = new FileOutputStream(f, true);
content = request.getInputStream();
int read = 0;
final byte[] bytes = new byte[BUFFER_LENGTH];
while ((read = content.read(bytes)) != -1) {
out.write(bytes, 0, read);
try {
// 这里主要是控制读写速度,呈现给前端的是上传速度
Thread.sleep(ConstantByProperties.uploadSpeed);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
start = f.length();
} catch (StreamException se) {
success = StreamException.ERROR_FILE_RANGE_START == se.getCode();
message = "Code: " + se.getCode();
throw new StreamException(se.getCode());
} catch (FileNotFoundException fne) {
message = "Code: " + StreamException.ERROR_FILE_NOT_EXIST;
success = false;
throw new FileNotFoundException();
} catch (IOException io) {
message = "IO Error: " + io.getMessage();
success = false;
throw new IOException(io);
} finally {
IoUtil.close(out);
IoUtil.close(content);
/** rename the file */
Map<String, String> map = new HashMap<String, String>();
if (range.getSize() == start) {
/** fix the `renameTo` bug */
try {
String cgSubDir = ConstantByProperties.basePath;
String uuid = UUID.randomUUID().toString();
String fileSaveName = uuid + "." + fileName.split("\\.")[fileName.split("\\.").length - 1];
String url = cgSubDir + fileSaveName;
Path pathtrue = f.toPath().resolveSibling(url);
File parentFile = pathtrue.toFile().getParentFile();
if (!parentFile.exists()) {
parentFile.mkdirs();
}
Files.move(f.toPath(), pathtrue);
map.put("name", fileName);
map.put("uuid", uuid);
map.put("url", url);
// 获得上传后的文件对象,可以做一些后期的处理,比如生成图片缩略图,视频缩略图等等,最好用异步的方式去执行这些操作,开启异步配置自行百度
File newFile = pathtrue.toFile();
// 生成图片缩略图,生成缩略图存放的路径跟原图路径是相同的
imageAsync.createThumbnail(newFile);
// 异步生成视频缩略图,第二个参数表示取(视频长度/xx)的那一帧作为缩略图
videoAsync.randomGrabberFFmpegImage(pathtrue.toString(), 2);
} catch (IOException e) {
e.printStackTrace();
success = false;
message = "Rename file error: " + e.getMessage();
}
}
try {
if (success) {
json.put(START_FIELD, start);
json.put("map", map);
}
json.put(TokenController.SUCCESS, success);
json.put(TokenController.MESSAGE, message);
} catch (JSONException e) {
System.out.println(e.toString());
}
writer.write(json.toString());
IoUtil.close(writer);
}
}
private void doOptions(HttpServletRequest req, HttpServletResponse resp) throws UnsupportedEncodingException {
req.setCharacterEncoding("UTF-8");
resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=utf-8");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Headers", "Content-Range,Content-Type");
resp.setHeader("Access-Control-Allow-Origin", "*");
resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
}
}
demo地址:https://github.com/iceSnowChen/upload-demo
ps:由于公司有加密机制,demo中的java文件会被加密,我做了一下备份,自己改一下就行了