手上有个文件上传的需求,并且要支持断点续传最好要兼容性好一些,之前用过uploadify这个jquery上传插件,但是首先它不支持断点续传而且HTML5版本的竟然要收费,秉承中国特色这里就不予考虑了;于是在网上找到了一个叫
Stream的支持HTML5和Flash并且支持断点续传的这么一个插件,经过一天的尝试,终于把它整合到项目中去,现勉强能用了后续再优化优化应该能投入上线,废话不多上代码!
HTML+JS:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%><%String path = request.getContextPath();String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";%><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><base href="<%=basePath%>"><title>断点续传</title><link rel="stylesheet" type="text/css" href="<%=basePath%>/statics/thirdparty/stream/css/stream-v1.css"><script type="text/javascript" src="<%=basePath%>/statics/thirdparty/stream/js/stream-v1.js"></script></head><body><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.disable();">禁用文件选择</button>|<button onclick="javascript:_t.enable();">启用文件选择</button>|<button onclick="javascript:_t.hideBrowseBlock();">隐藏文件选择按钮</button>|<button onclick="javascript:_t.showBrowseBlock();">显示文件选择按钮</button>|<button onclick="javascript:_t.destroy();_t=null;_t=new Stream(config);">销毁重新生成按钮</button><br>Messages:<div id="i_stream_message_container" class="stream-main-upload-box" style="overflow: auto;height:200px;"></div></body><script type="text/javascript">/*** 配置文件(如果没有默认字样,说明默认值就是注释下的值)* 但是,on*(onSelect, onMaxSizeExceed...)等函数的默认行为* 是在ID为i_stream_message_container的页面元素中写日志*/var config = {browseFileId : "i_select_files", /** 选择文件的ID, 默认: i_select_files */browseFileBtn : "<div>请选择文件</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 */maxSize: 4294967296, /** 单个文件的最大大小,默认:2G */multipleFiles: false, /** 多个文件一起上传, 默认: false */autoUploading: true, /** 选择文件后是否自动上传, 默认: true */// autoRemoveCompleted : true, /** 是否自动删除容器中已上传完毕的文件, 默认: false */// retryCount : 5, /** HTML5上传失败的重试次数 */// postVarsPerFile : { /** 上传文件时传入的参数,默认: {} */// param1: "val1",// param2: "val2"// },// swfURL : "/swf/FlashUploader.swf", /** SWF文件的位置 */tokenURL : "<%=request.getContextPath()%>/tk", /** 根据文件名、大小等信息获取Token的URI(用于生成断点续传、跨域的令牌) */// frmUploadURL : "/fd;", /** Flash上传的URI */uploadURL : "<%=request.getContextPath()%>/upload", /** HTML5上传的URI */// simLimit: 200, /** 单次最大上传文件个数 */// extFilters: [".txt", ".rpm", ".rmvb", ".gz", ".rar", ".zip", ".avi", ".mkv", ".mp3"], /** 允许的文件扩展名, 默认: [] */// onSelect: function(list) {alert('onSelect')}, /** 选择文件后的响应事件 */onMaxSizeExceed: function(size, limited, name) {alert('文件已超过4G');}, /** 文件大小超出的响应事件 */// onFileCountExceed: function(selected, limit) {alert('onFileCountExceed')}, /** 文件数量超出的响应事件 */// onExtNameMismatch: function(name, filters) {alert('onExtNameMismatch')}, /** 文件的扩展名不匹配的响应事件 */// onCancel : function(file) {alert('Canceled: ' + file.name)}, /** 取消上传文件的响应事件 */// onComplete: function(file) {alert('onComplete')}, /** 单个文件上传完毕的响应事件 */onQueueComplete: function() {alert('onQueueComplete');}, /** 所以文件上传完毕的响应事件 */// onUploadError: function(status, msg) {alert('onUploadError')} /** 文件上传出错的响应事件 */// onDestroy: function() {alert('onDestroy')} /** 文件上传出错的响应事件 */};var _t = new Stream(config);</script></html>
页面代码精简了下,只留下了关键部分。
TokenServlet:
package com.dnion.oa.stream.servlet;import java.io.IOException;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.apache.commons.lang.StringUtils;import org.json.JSONException;import org.json.JSONObject;import com.dnion.oa.stream.util.IoUtil;import com.dnion.oa.stream.util.TokenUtil;import com.dnion.oa.utils.Configuration;import com.dnion.oa.utils.Constant;/*** According the file name and its size, generate a unique token. And this* token will be refer to user's file.*/public class TokenServlet extends HttpServlet {private static final long serialVersionUID = 2650340991003623753L;static final String FILE_NAME_FIELD = "name";static final String FILE_SIZE_FIELD = "size";static final String TOKEN_FIELD = "token";static final String SERVER_FIELD = "server";static final String SUCCESS = "success";static final String MESSAGE = "message";@Overridepublic void init() throws ServletException {}@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {String name = StringUtils.trimToEmpty(req.getParameter(FILE_NAME_FIELD));String size = StringUtils.trimToEmpty(req.getParameter(FILE_SIZE_FIELD));String token = TokenUtil.generateToken(name, size);PrintWriter writer = resp.getWriter();JSONObject json = new JSONObject();try {if (name.equals("")) {json.put(SUCCESS, false);json.put(MESSAGE, "文件名为空");}else if (size.equals("")) {json.put(SUCCESS, false);json.put(MESSAGE, "文件大小为空");}else{json.put(TOKEN_FIELD, token);if (Configuration.getInstance().getIsCrossed())json.put(SERVER_FIELD, Configuration.getInstance().getCrossServer());json.put(SUCCESS, true);json.put(MESSAGE, "获取TOKEN成功");}} catch (JSONException e) {e.printStackTrace();}/** TODO: save the token. */writer.write(json.toString());}@Overrideprotected void doHead(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {super.doHead(req, resp);}@Overridepublic void destroy() {super.destroy();}}
这是实现上传功能必须实现接口之一,看名字就知道基本功能就是获取token实现‘断点‘功能
StreamServlet:
package com.dnion.oa.stream.servlet;import java.io.File;import java.io.FileNotFoundException;import java.io.FileOutputStream;import java.io.IOException;import java.io.InputStream;import java.io.OutputStream;import java.io.PrintWriter;import javax.servlet.ServletException;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import org.json.JSONException;import org.json.JSONObject;import com.dnion.oa.stream.util.IoUtil;import com.dnion.oa.utils.Configuration;/*** File reserved servlet, mainly reading the request parameter and its file* part, stored it.*/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";@Overridepublic void init() throws ServletException {}/*** Lookup where's the position of this file?*/@Overrideprotected void doGet(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {doOptions(req, resp);final String token = req.getParameter(TokenServlet.TOKEN_FIELD);final String size = req.getParameter(TokenServlet.FILE_SIZE_FIELD);final String fileName = req.getParameter(TokenServlet.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();/** file size is 0 bytes. */if (token.endsWith("_0") && "0".equals(size) && 0 == start)f.renameTo(IoUtil.getFile(fileName));} catch (FileNotFoundException fne) {message = "Error: " + fne.getMessage();success = false;} finally {try {if (success)json.put(START_FIELD, start);json.put(TokenServlet.SUCCESS, success);json.put(TokenServlet.MESSAGE, message);} catch (JSONException e) {}writer.write(json.toString());IoUtil.close(writer);}}@Overrideprotected void doPost(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {doOptions(req, resp);final String token = req.getParameter(TokenServlet.TOKEN_FIELD);final String fileName = req.getParameter(TokenServlet.FILE_NAME_FIELD);Range range = IoUtil.parseRange(req);OutputStream out = null;InputStream content = null;final PrintWriter writer = resp.getWriter();/** 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();f.renameTo(dst);System.out.println("TK: `" + token + "`, NE: `" + fileName + "`");/** if `STREAM_DELETE_FINISH`, then delete it. */if (Configuration.getInstance().getIsDeleteFinished()) {dst.delete();}}try {if (success)json.put(START_FIELD, start);json.put(TokenServlet.SUCCESS, success);json.put(TokenServlet.MESSAGE, message);} catch (JSONException e) {}writer.write(json.toString());IoUtil.close(writer);}}@Overrideprotected void doOptions(HttpServletRequest req, HttpServletResponse resp)throws ServletException, IOException {resp.setContentType("application/json");resp.setHeader("Access-Control-Allow-Headers", "Content-Range,Content-Type");resp.setHeader("Access-Control-Allow-Origin", Configuration.getInstance().getCrossOrigins());resp.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");}@Overridepublic void destroy() {super.destroy();}}
必须实现接口之一,也是最为主要的一个servlet
这两个接口其实就是对应了js代码中tokenURL和uploadURL,跳转关系在web.xml中定义,把下面这段加进去就行了,记得改路径
<servlet><servlet-name>TokenServlet</servlet-name><servlet-class>com.dnion.oa.stream.servlet.TokenServlet</servlet-class><load-on-startup>0</load-on-startup></servlet><servlet-mapping><servlet-name>TokenServlet</servlet-name><url-pattern>/tk</url-pattern></servlet-mapping><servlet><servlet-name>StreamServlet</servlet-name><servlet-class>com.dnion.oa.stream.servlet.StreamServlet</servlet-class><load-on-startup>0</load-on-startup></servlet><servlet-mapping><servlet-name>StreamServlet</servlet-name><url-pattern>/upload</url-pattern></servlet-mapping><servlet><servlet-name>FormDataServlet</servlet-name><servlet-class>com.dnion.oa.stream.servlet.FormDataServlet</servlet-class><load-on-startup>0</load-on-startup></servlet><servlet-mapping><servlet-name>FormDataServlet</servlet-name><url-pattern>/fd</url-pattern></servlet-mapping>
基本参数保存在properties文件中
stream-config.properties:
# file stored repository (Chinese words need ASCII, help tool @http://tool.oschina.net/encode?type=3)STREAM_FILE_REPOSITORY=# when the file has uploaded, whether delete it.STREAM_DELETE_FINISH=false# this server whether allow other different domain[s] upload file to this serverSTREAM_IS_CROSS=false# allowed domain (PS: flash method need modifying the `crossdomain.xml`)STREAM_CROSS_ORIGIN=*# when Browser @http:www.A.com, the file will upload to @STREAM_CROSS_SERVERSTREAM_CROSS_SERVER=http://customers.duapp.comTEMP_UPLOAD_PATH=E\:\\resumeUpload //暂时放在本地 测试用
Configuration.java读取配置文件
Configuration.java:
package com.dnion.oa.utils;import org.apache.commons.configuration.ConfigurationException;import org.apache.commons.configuration.PropertiesConfiguration;public class Configuration {private static Configuration config;/*** 版本号*/private String version;private String crossServer;private String crossOrigins;private String dnionTranscoderUrl;private Boolean isDeleteFinished;private Boolean isCrossed;private String uploadPath;private Configuration() {}public synchronized static Configuration getInstance() {if (config == null) {config = new Configuration();config.init();}return config;}/*** 初始化*/private void init() {PropertiesConfiguration prop = new PropertiesConfiguration();prop.setEncoding("utf-8");try {prop.load("props/stream-config.properties");this.setCrossServer(prop.getString("STREAM_CROSS_SERVER", ""));this.setCrossOrigins(prop.getString("STREAM_CROSS_ORIGIN", ""));this.setDnionTranscoderUrl(prop.getString("DNION_TRANS_CODER_URL",""));this.setIsDeleteFinished(Boolean.valueOf(prop.getString("STREAM_DELETE_FINISH", "")));this.setIsCrossed(Boolean.valueOf(prop.getString("STREAM_IS_CROSS","")));this.setChownShellCmd(prop.getString("CHOWN_SHELL_CMD", ""));this.setUploadPath(prop.getString("TEMP_UPLOAD_PATH", ""));} catch (ConfigurationException e) {e.printStackTrace();}}public String getVersion() {return version;}public void setVersion(String version) {this.version = version;}...getter and setters还有6对,别忘了}
Maven依赖:
<dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.4</version></dependency><dependency><groupId>org.json</groupId><artifactId>json</artifactId><version>20090211</version></dependency><dependency><groupId>commons-configuration</groupId><artifactId>commons-configuration</artifactId><version>1.9</version></dependency>
其他还有一些TokenUtil.java就不给出了,作者代码托管在oschina上,自己拉了看着改吧~
效果图稍候放出: