方块糖的工坊
方块糖的工坊

Stream+SpringMVC实现文件断点续传

手上有个文件上传的需求,并且要支持断点续传最好要兼容性好一些,之前用过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";

    @Override
    public void init() throws ServletException {
    }

    @Override
    protected 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());
    }

    @Override
    protected void doHead(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        super.doHead(req, resp);
    }

    @Override
    public 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";
    @Override
    public void init() throws ServletException {
    }

    /**
     * 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(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") &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; "0".equals(size) &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; 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);
        }
    }

    @Override
    protected 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);
        }
    }

    @Override
    protected 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");
    }

    @Override
    public 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 server
STREAM_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_SERVER
STREAM_CROSS_SERVER=http://customers.duapp.com
TEMP_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>

发表回复

textsms
account_circle
email

方块糖的工坊

Stream+SpringMVC实现文件断点续传
手上有个文件上传的需求,并且要支持断点续传最好要兼容性好一些,之前用过uploadify这个jquery上传插件,但是首先它不支持断点续传而且HTML5版本的竟然要收费,秉承中国特色这里就不予考…
扫描二维码继续阅读
2015-10-29