昨天刚刚做了一个文件列表上传,后端很简单,用
MultipartFile[] files
获取文件流数组,后端就当IO流操作就可以,似乎好像没啥好写的,但是!!!!!前端是真的糙单.要是自己写一个前端单个文件上传样式是丑了点,不过还是能用的,只是样式是真的丑了....无语了,所有有了这篇.首先来张完成的效果
下面就是实现步骤了,开始对比了Bootstrap fileinput 和jQeury的uploadfile,我使用的功能似乎单一且简单,所以并不需要哪些花狸狐哨的功能,所以选择了这个插件,首先还是感谢大佬,开源这么好的插件
DEMO地址: http://w.twinkling.cn/ 官网地址: http://www.twinkling.cn/
我使用的是SpringBoot,上面的demo是基于基本的servlet写的,现在需要整合到我的项目中.
这个需要注意一点,插件需要写一个
/tk 请求,用于生层上传文件的唯一TOKEN,标识文件,其他的还需要一个配置类,基本配置文件,涉及到文件上传的一些配置,等下一起给出来
前端代码:
<head> <meta charset="UTF-8"> <title>上传数据</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <link href="../../static/files/css/stream-v1.css" rel="stylesheet" type="text/css"> </head> <style> body{ 90%; padding: 10px; margin-left: 5%; } #i_select_files{ 70%; height: 5rem; } #i_stream_message_container{ position: absolute !important; float: left !important; } #operate{ position: absolute; 50%; height: 100%; } #result{ position: absolute; 50%; height: 100%; margin-left: 50%; } button{ background: transparent; border: 1px #2a6496 solid; 3rem; margin-left: 2.8rem; margin-top: 2rem; border-radius: 5px; background-color: #0099FF; font-weight: bolder; } button:hover{ background-color: #4cae4c; pointer-events: painted; } #i_select_files div{ margin-top: 1rem; } </style> <body> <div id="operate"> <div id="i_select_files"></div> <div id="i_stream_files_queue"></div> <button onclick="javascript:_t.upload();">开始上传</button> <button onclick="javascript:_t.stop();">停止上传</button> <button onclick="javascript:_t.cancel();">取消上传</button> <button onclick="javascript:_t.destroy();_t=null;_t=new Stream(config);">重新上传</button> <button id="import" style="color: #c9302c">导入文件</button> </div> <div id="result"> 结果信息: <div id="i_stream_message_container" class="stream-main-upload-box" style="overflow: auto;height: 93%;color: #3c763d"></div> </div> </body> <script type="text/javascript" src="../../static/js/jquery-1.11.3.min.js"></script> <script type="text/javascript" src="../../static/files/js/stream-v1.js"></script> <script src="https://cdn.bootcss.com/layer/2.3/layer.js"></script> <script type="text/javascript"> $("#import").click(function () { //询问框 layer.confirm('确定导入这个月最新上传的数据?', { btn: ['确定','我在想想'] //按钮 }, function(){ //加载层 var index = layer.load(0, {shade: false}); //0代表加载的风格,支持0-2 $.ajax({ type: "GET", url: "/xlxs/setData", success: function (data) { if(data != null){ layer.close(index); layer.alert('导入成功', { skin: 'layui-layer-lan' ,closeBtn: 0 ,anim: 4 //动画类型 }); }else{ layer.msg("导入失败,请重新导入!"); } } }); }, function(){ }); }) /** * 配置文件(如果没有默认字样,说明默认值就是注释下的值) * 但是,on*(onSelect, onMaxSizeExceed...)等函数的默认行为 * 是在ID为i_stream_message_container的页面元素中写日志 */ var config = { browseFileId : "i_select_files", /** 选择文件的ID, 默认: i_select_files */ browseFileBtn : "<div>请把xlxs文件拖到这里</div>", /** 显示选择文件的样式, 默认: <div>请选择文件</div> */ dragAndDropArea: "i_select_files", /** 拖拽上传区域,Id(字符类型"i_select_files")或者DOM对象, 默认: `i_select_files` */ dragAndDropTips: "<span>(文件夹)也是可以的</span>", /** 拖拽提示, 默认: <span>把文件(文件夹)拖拽到这里</span> */ filesQueueId : "i_stream_files_queue", /** 文件上传容器的ID, 默认: i_stream_files_queue */ filesQueueHeight : 200, /** 文件上传容器的高度(px), 默认: 450 */ messagerId : "i_stream_message_container", /** 消息显示容器的ID, 默认: i_stream_message_container */ multipleFiles: true, /** 多个文件一起上传, 默认: false */ onRepeatedFile: function(f) { alert("文件:"+f.name +" 大小:"+f.size + " 已存在于上传队列中。"); return false; }, autoUploading: false, /** 选择文件后是否自动上传, 默认: true */ autoRemoveCompleted : true, /** 是否自动删除容器中已上传完毕的文件, 默认: false */ maxSize: 20480000, /** 单个文件的最大大小,默认:2G */ retryCount : 3, /** HTML5上传失败的重试次数 */ // postVarsPerFile : { /** 上传文件时传入的参数,默认: {} */ // param1: "val1", // param2: "val2" // }, // swfURL : "/swf/FlashUploader.swf", /** SWF文件的位置 */ // tokenURL : "/tk", /** 根据文件名、大小等信息获取Token的URI(用于生成断点续传、跨域的令牌) */ // frmUploadURL : "/fd;", /** Flash上传的URI */ uploadURL : "/upload", /** HTML5上传的URI */ simLimit: 50, /** 单次最大上传文件个数 */ extFilters: [".xlsx"], /** 允许的文件扩展名, 默认: [] */ // onSelect: function(list) {alert('onSelect')}, /** 选择文件后的响应事件 */ onMaxSizeExceed: function(size, limited, name) { alert("上传文件太大了,支持20MB以下") }, /** 文件大小超出的响应事件 */ onFileCountExceed: function(selected, limit) { alert("最大上传数量是50个"); }, /** 文件数量超出的响应事件 */ onExtNameMismatch: function(name, filters) { alert(file.name+' 的文件格式不对,换个试试[xlsx]') }, /** 文件的扩展名不匹配的响应事件 */ // onCancel : function(file) { // // }, /** 取消上传文件的响应事件 */ // onComplete: function(file) {alert('onComplete')}, /** 单个文件上传完毕的响应事件 */ onQueueComplete: function() { _t.destroy();_t=null;_t=new Stream(config); }, /** 所有文件上传完毕的响应事件 */ onUploadError: function(status, msg) { alert('上传失败') }, /** 文件上传出错的响应事件 */ onDestroy: function() { } /** 文件上传出错的响应事件 */ }; var _t = new Stream(config); </script>
这里面需要获取两个文件
stream-v1.css stream-v1.js
其实这个也好获取,要是大家拿不到的话,我就发出来,其他的例子上说明的很详细,按照自己的业务要求修改就可以了.
后台代码:
这里是按照官网给的案例,整合到自己的SpringBoot项目中的,只是稍微修改了下代码,就可以了,只是需要找到修改的地方即可,要是找不到,呵呵,那就又要花费一天的干活.
好啦,开始....基于MVC模式
Controller
@GetMapping("/upload") public void getUpload(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { StreamServlet streamServlet = new StreamServlet(); streamServlet.doGet(request,response); } @PostMapping("/upload") public void postUpload(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { StreamServlet streamServlet = new StreamServlet(); streamServlet.doPost(request,response); }
streamServlet:
public class StreamServlet extends HttpServlet { private static final long serialVersionUID = -8619685235661387895L; /** when the has increased to 10kb, then flush it to the hard-disk. */ static final int BUFFER_LENGTH = 10240; static final String START_FIELD = "start"; public static final String CONTENT_RANGE_HEADER = "content-range"; /** * Lookup where's the position of this file? */ @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doOptions(req, resp); final String token = req.getParameter(UploadController.TOKEN_FIELD); final String size = req.getParameter(UploadController.FILE_SIZE_FIELD); final String fileName = req.getParameter(UploadController.FILE_NAME_FIELD); final PrintWriter writer = resp.getWriter(); /** TODO: validate your token. */ JSONObject json = new JSONObject(); long start = 0; boolean success = true; String message = ""; try { File f = IoUtil.getTokenedFile(token); start = f.length(); } finally { try { if (success) json.put(START_FIELD, start); json.put(UploadController.SUCCESS, success); json.put(UploadController.MESSAGE, message); } catch (JSONException e) {} writer.write(json.toString()); IoUtil.close(writer); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doOptions(req, resp); final String token = req.getParameter(UploadController.TOKEN_FIELD); final String fileName = req.getParameter(UploadController.FILE_NAME_FIELD); Range range = IoUtil.parseRange(req); OutputStream out = null; InputStream content = null; final PrintWriter writer = resp.getWriter(); //清除旧的文件 if(!IoUtil.deleteFile(fileName)){ writer.write("上传失败"); } /** TODO: validate your token. */ 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 = req.getInputStream(); int read = 0; final byte[] bytes = new byte[BUFFER_LENGTH]; while ((read = content.read(bytes)) != -1) out.write(bytes, 0, read); start = f.length(); }catch (StreamException se) { success = StreamException.ERROR_FILE_RANGE_START == se.getCode(); message = "Code: " + se.getCode(); }catch (FileNotFoundException fne) { message = "Code: " + StreamException.ERROR_FILE_NOT_EXIST; success = false; } catch (IOException io) { message = "IO Error: " + io.getMessage(); success = false; } finally { IoUtil.close(out); IoUtil.close(content); /** rename the file */ if (range.getSize() == start) { /** fix the `renameTo` bug */ // File dst = IoUtil.getFile(fileName); // dst.delete(); // TODO: f.renameTo(dst); 重命名在Windows平台下可能会失败,stackoverflow建议使用下面这句 try { // 先删除 IoUtil.getFile(fileName).delete(); Files.move(f.toPath(), f.toPath().resolveSibling(fileName)); System.out.println("TK: `" + token + "`, NE: `" + fileName + "`"); /** if `STREAM_DELETE_FINISH`, then delete it. */ if (Configurations.isDeleteFinished()) { IoUtil.getFile(fileName).delete(); } } catch (IOException e) { success = false; message = "Rename file error: " + e.getMessage(); } } try { if (success) { json.put(START_FIELD, start); } json.put(UploadController.SUCCESS, success); json.put(UploadController.MESSAGE, message); } catch (JSONException e) {} writer.write(json.toString()); IoUtil.close(writer); } } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("application/json;charset=utf-8"); resp.setHeader("Access-Control-Allow-Headers", "Content-Range,Content-Type"); resp.setHeader("Access-Control-Allow-Origin", Configurations.getCrossOrigins()); resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); } @Override public void destroy() { super.destroy(); } }
IoUtils:
/** * IO--closing, getting file name ... main function method */ public class IoUtil { static final Pattern RANGE_PATTERN = Pattern.compile("bytes \d+-\d+/\d+"); /** * According the key, generate a file (if not exist, then create * a new file). * * @param filename * @return * @throws IOException */ public static File getFile(String filename) throws IOException { if (filename == null || filename.isEmpty()) return null; String name = filename.replaceAll("/", Matcher.quoteReplacement(File.separator)); File f = new File(Configurations.getFileRepository() + File.separator + name); if (!f.getParentFile().exists()) f.getParentFile().mkdirs(); if (!f.exists()) f.createNewFile(); return f; } /** * 清楚旧的文件 * * @param fileName * @return */ public static boolean deleteFile(String fileName){ boolean flag = false; File file = new File(new UploadServiceImpl().filePathForNowDay() + File.separator + fileName); if(file.exists()){ return file.delete(); } return flag; } /** * Acquired the file. * * @param key * @return * @throws IOException */ public static File getTokenedFile(String key) throws IOException { if (key == null || key.isEmpty()) return null; // 文件保存在服务器上的路径 File f = new File(new UploadServiceImpl().filePathForNowDay().toString()+key); if (!f.getParentFile().exists()) f.getParentFile().mkdirs(); if (!f.exists()) f.createNewFile(); return f; } public static void storeToken(String key) throws IOException { if (key == null || key.isEmpty()) return; File f = new File(Configurations.getFileRepository() + File.separator + key); if (!f.getParentFile().exists()) f.getParentFile().mkdirs(); if (!f.exists()) f.createNewFile(); } /** * close the IO stream. * * @param stream */ public static void close(Closeable stream) { try { if (stream != null) stream.close(); } catch (IOException e) { } } /** * 获取Range参数 * * @param req * @return * @throws IOException */ public static Range parseRange(HttpServletRequest req) throws IOException { String range = req.getHeader(StreamServlet.CONTENT_RANGE_HEADER); Matcher m = RANGE_PATTERN.matcher(range); if (m.find()) { range = m.group().replace("bytes ", ""); String[] rangeSize = range.split("/"); String[] fromTo = rangeSize[0].split("-"); long from = Long.parseLong(fromTo[0]); long to = Long.parseLong(fromTo[1]); long size = Long.parseLong(rangeSize[1]); return new Range(from, to, size); } throw new IOException("Illegal Access!"); } /** * From the InputStream, write its data to the given file. */ public static long streaming(InputStream in, String key, String fileName) throws IOException { OutputStream out = null; File f = getTokenedFile(key); try { out = new FileOutputStream(f); int read = 0; final byte[] bytes = new byte[FormDataServlet.BUFFER_LENGTH]; while ((read = in.read(bytes)) != -1) { out.write(bytes, 0, read); } out.flush(); } finally { close(out); } /** rename the file * fix the `renameTo` bug */ File dst = IoUtil.getFile(fileName); dst.delete(); f.renameTo(dst); long length = getFile(fileName).length(); /** if `STREAM_DELETE_FINISH`, then delete it. */ if (Configurations.isDeleteFinished()) { dst.delete(); } return length; } }
TokenUtil:
/** * Key Util: 1> according file name|size ..., generate a key; * 2> the key should be unique. */ public class TokenUtil { /** * 生成Token, A(hashcode>0)|B + |name的Hash值| +_+size的值 * @param name * @param size * @return * @throws Exception */ public static String generateToken(String name, String size) throws IOException { if (name == null || size == null) return ""; int code = name.hashCode(); try { String token = (code > 0 ? "A" : "B") + Math.abs(code) + "_" + size.trim(); /** TODO: store your token, here just create a file */ IoUtil.storeToken(token); return token; } catch (Exception e) { throw new IOException(e); } } }
.............................直接看仓库吧.
官方仓库:https://gitee.com/jiangdx/stream
基本上,看完这些可以缩短40%的时间...哈哈