SpringBoot实现断点续传_springboot 断点续传-程序员宅基地

技术标签: spring boot  SpringBoot  后端  p2p  

    大文件面临的问题
    
        1. 上传速度慢 -- 应对: 分块上传
        2. 上传文件到一半中断后,继续上传却只能重头开始上传 -- 应对: 断点续传
        3. 相同文件未修改再次上传, 却只能重头开始上传 -- 应对: 秒传

分片上传

1、什么分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件

2、分片上传适用场景

  1. 大文件上传
  2. 网络环境环境不好,存在需要重传风险的场景

3、上传的具体流程

因为这个上传流程和断点续传类似,就在下边介绍断点续传中介绍

断点续传

1、什么是断点续传

断点续传是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传或者下载未完成的部分,而没有必要从头开始上传或者下载。本文的断点续传主要是针对断点上传场景

2、应用场景

断点续传可以看成是分片上传的一个衍生,因此可以使用分片上传的场景,都可以使用断点续传

3、实现断点续传的核心逻辑

在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传。

为了避免客户端在上传之后的进度数据被删除而导致重新开始从头上传的问题,服务端也可以提供相应的接口便于客户端对已经上传的分片数据进行查询,从而使客户端知道已经上传的分片数据,从而从下一个分片数据开始继续上传。

4、实现流程步骤

  • 前端(客户端)需要根据固定大小对文件进行分片,请求后端(服务端)时要带上分片序号和大小
  • 服务端创建conf文件用来记录分块位置,conf文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认的0,已上传的就是Byte.MAX_VALUE 127(这步是实现断点续传和秒传的核心步骤)
  • 服务器按照请求数据中给的分片序号和每片分块大小(分片大小是固定且一样的)算出开始位置,与读取到的文件片段数据,写入文件

秒传

1、什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了

2、本文实现的秒传核心逻辑

a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,

b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径

WebUploader

1,什么是 WebUploader?

WebUploader 是由百度公司团队开发的一个以 HTML5 为主,FLASH 为辅的现代文件上传组件。

2,功能特点

  • 分片、并发:WebUploader 采用大文件分片并发上传,极大的提高了文件上传效率
  • 预览、压缩:WebUploader 支持常用图片格式 jpg,jpeg,gif,bmp,png 预览与压缩,节省网络数据传输。
  • 多途径添加文件:支持文件多选,类型过滤,拖拽(文件 & 文件夹),图片粘贴功能。
  • HTML5 & FLASH:兼容主流浏览器,接口一致,实现了两套运行时支持,用户无需关心内部用了什么内核。
  • MD5 秒传:当文件体积大、量比较多时,支持上传前做文件 md5 值验证,一致则可直接跳过
  • 易扩展、可拆分:采用可拆分机制, 将各个功能独立成了小组件,可自由搭配。

3. 接口说明

  • before-send-file 此hook在文件发送之前执行
  • before-file 此hook在文件分片(如果没有启用分片,整个文件被当成一个分片)后,上传之前执行。
  • after-send-file 此hook在文件所有分片都上传完后,且服务端没有错误返回后执行。

Web Uploader的所有代码都在一个内部闭包中,对外暴露了唯一的一个变量WebUploader,所以完全不用担心此框架会与其他框架冲突。

内部所有的类和功能都暴露在WebUploader名字空间下面。

Demo中使用的是WebUploader.create方法来初始化的,实际上可直接访问WebUploader.Uploader

var uploader = new WebUploader.Uploader({
    swf: 'path_of_swf/Uploader.swf'
    // 其他配置项
});

具体有哪些内部类,请转到API页面。

4. 事件

Uploader实例具有Backbone同样的事件API:onoffoncetrigger

uploader.on( 'fileQueued', function( file ) {
    // do some things.
});

除了通过on绑定事件外,Uploader实例还有一个更便捷的添加事件方式。

uploader.onFileQueued = function( file ) {
    // do some things.
};

如同Document Element中的onEvent一样,他的执行比on添加的handler的要晚。如果那些handler里面,有一个return false了,此onEvent里面是不会执行到的。

5. Hook

Uploader里面的功能被拆分成了好几个widget,由command机制来通信合作。

如下,filepicker在用户选择文件后,直接把结果request出去,然后负责队列的queue widget,监听命令,根据配置项中的accept来决定是否加入队列。

// in file picker
picker.on( 'select', function( files ) {
    me.owner.request( 'add-file', [ files ]);
});
// in queue picker
Uploader.register({
    'add-file': 'addFiles'
    // xxxx
}, {
    addFiles: function( files ) {
        // 遍历files中的文件, 过滤掉不满足规则的。
    }
});

Uploader.regeister方法用来说明,该widget要响应哪些命令,并指定由什么方法来响应。上面的例子,当add-file命令派送时,内部的addFiles成员方法将被执行到,同一个命令,可以指定多次handler, 各个handler会按添加顺序依次执行,且后续的handler,不能被前面的handler截断。

handler里面可以是同步过程,也可以是异步过程。是异步过程时,只需要返回一个promise对象即可。存在异步可能的request调用者会等待此过程结束后才继续。举个例子,webuploader运行在flash模式下时,需要等待flash加载完毕后才能算ready了,此过程为一个异步过程,目前的做法是如下:

// uploader在初始化的时候
me.request( 'init', opts, function() {
    me.state = 'ready';
    me.trigger('ready');
});
// filepicker `widget`中的初始化过程。
Uploader.register({
    'init': 'init'
}, {
    init: function( opts ) {
        var deferred = Base.Deferred();
        // 加载flash
        // 当flash ready执行deferred.resolve方法。
        return deferred.promise();
    }
});

目前webuploader内部有很多种command,在此列出比较重要的几个。

名称 参数 说明
add-file files: File对象或者File数组 用来向队列中添加文件。
before-send-file file: File对象 在文件发送之前request,此时还没有分片(如果配置了分片的话),可以用来做文件整体md5验证。
before-send block: 分片对象 在分片发送之前request,可以用来做分片验证,如果此分片已经上传成功了,可返回一个rejected promise来跳过此分片上传
after-send-file file: File对象 在所有分片都上传完毕后,且没有错误后request,用来做分片验证,此时如果promise被reject,当前文件上传会触发错误。

代码实现:基于SpringBoot和WebUploader

前端页面(WebUploader.html)

<html>
<head>
    <meta charset="utf-8">
    <title>BigFile-WebUploader</title>
    <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
    <link rel="stylesheet" href="/css/webuploader.css">
    <script type="text/javascript" src="http://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script>
    <script type="text/javascript" src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
    <script type="text/javascript" src="/js/webuploader.js"></script>
</head>
<body>
<div id="uploader" class="wu-example">
    <div id="thelist" class="uploader-list"></div>
    <div class="btns">
        <div id="picker">选择大文件</div>
        <button id="ctlBtn" class="btn btn-default">开始上传</button>
        <button id="stopBtn" class="btn btn-default">暂停</button>
        <button id="restart" class="btn btn-default">开始</button>
    </div>
</div>
</body>
<!--业务js文件-->
<script>
    var $btn = $('#ctlBtn');
    var $thelist = $('#thelist');
    var startDate;
    // HOOK 这个必须要再uploader实例化前面
    WebUploader.Uploader.register({
        // 在文件发送之前执行
        'before-send-file': 'beforeSendFile',
        // 在文件分片(如果没有启用分片,整个文件被当成一个分片)后,上传之前执行
        'before-send': 'beforeSend',
        // 在文件所有分片都上传完后,且服务端没有错误返回后执行
        "after-send-file": "afterSendFile"
    }, {
        beforeSendFile: function (file) {
            startDate = new Date();
            console.log("开始上传时间" + startDate)
            console.log("beforeSendFile");
            // Deferred对象在钩子回掉函数中经常要用到,用来处理需要等待的异步操作。
            var deferred = WebUploader.Deferred();
            //1、计算文件的唯一标记MD5,用于断点续传
            uploader.md5File(file, 0, 3 * 1024 * 1024).progress(function (percentage) {
                // 上传进度
                console.log('上传进度:', percentage);
                getProgressBar(file, percentage, "MD5", "MD5");
            }).then(function (val) { // 完成
                console.log('File MD5 Result:', val);
                file.md5 = val;
                file.uid = WebUploader.Base.guid();
                // 判断文件是否上传过,是否存在分片,断点续传
                $.ajax({
                    type: "POST",
                    url: "bigfile/check",
                    async: false,
                    data: {
                        fileMd5: val
                    },
                    success: function (data) {
                        var resultCode = data.resultCode;
                        // 秒传
                        if (resultCode == -1) {
                            // 文件已经上传过,忽略上传过程,直接标识上传成功;
                            uploader.skipFile(file);
                            file.pass = true;
                        } else {
                            //文件没有上传过,下标为0
                            //文件上传中断过,返回当前已经上传到的下标
                            file.indexcode = resultCode;
                        }
                    }, error: function () {
                    }
                });
                //获取文件信息后进入下一步
                deferred.resolve();
            });
            return deferred.promise();
        },
        beforeSend: function (block) {
            //获取已经上传过的下标
            var indexchunk = block.file.indexcode;
            var deferred = WebUploader.Deferred();
            if (indexchunk > 0) {
                if (block.chunk > indexchunk) {
                    //分块不存在,重新发送该分块内容
                    deferred.resolve();
                } else {
                    //分块存在,跳过
                    deferred.reject();
                }
            } else {
                //分块不存在,重新发送该分块内容
                deferred.resolve();
            }
            //返回Deferred的Promise对象。
            return deferred.promise();
        }
        , afterSendFile: function (file) {
            //如果所有分块上传成功,则通知后台合并分块
            $.ajax({
                type: "POST",
                url: "bigfile/merge",
                data: {
                    fileName: file.name,
                    fileMd5: file.md5
                },
                success: function (data) {
                }, error: function () {
                }
            });
        }
    });
    // 实例化
    var uploader = WebUploader.create({
        pick: {
            id: '#picker',
            label: '点击选择文件'
        },
        duplicate: true,//去重, 根据文件名字、文件大小和最后修改时间来生成hash Key
        swf: 'js/Uploader.swf',
        chunked: true,
        chunkSize: 10 * 1024 * 1024, // 10M 每个分片的大小限制
        threads: 3,
        server: 'bigfile/upload',
        auto: true,
        // 禁掉全局的拖拽功能。这样不会出现图片拖进页面的时候,把图片打开。
        disableGlobalDnd: true,
        fileNumLimit: 1024,
        fileSizeLimit: 50 * 1024 * 1024 * 1024,//50G 验证文件总大小是否超出限制, 超出则不允许加入队列
        fileSingleSizeLimit: 10 * 1024 * 1024 * 1024 //10G 验证单个文件大小是否超出限制, 超出则不允许加入队列
    });
    // 当有文件被添加进队列的时候
    uploader.on('fileQueued', function (file) {
        $thelist.append('<div id="' + file.id + '" class="item">' +
            '<h4 class="info">' + file.name + '</h4>' +
            '<p class="state">等待上传...</p>' +
            '</div>');
        $("#stopBtn").click(function () {
            uploader.stop(true);
        });
        $("#restart").click(function () {
            uploader.upload(file);
        });
    });
    //当某个文件的分块在发送前触发,主要用来询问是否要添加附带参数,大文件在开起分片上传的前提下此事件可能会触发多次。
    uploader.onUploadBeforeSend = function (obj, data) {
        //console.log("onUploadBeforeSend");
        var file = obj.file;
        data.md5 = file.md5 || '';
        data.uid = file.uid;
    };
    // 上传中
    uploader.on('uploadProgress', function (file, percentage) {
        getProgressBar(file, percentage, "FILE", "上传进度");
    });
    // 上传返回结果
    uploader.on('uploadSuccess', function (file) {
        var endDate = new Date();
        console.log("文件上传耗时:" + (endDate - startDate) / 1000 + "s")
        var text = '已上传';
        if (file.pass) {
            text = "文件妙传功能,文件已上传。"
        }
        $('#' + file.id).find('p.state').text(text);
    });
    uploader.on('uploadError', function (file) {
        $('#' + file.id).find('p.state').text('上传出错');
    });
    uploader.on('uploadComplete', function (file) {
        // 隐藏进度条
        fadeOutProgress(file, 'MD5');
        fadeOutProgress(file, 'FILE');
    });
    // 文件上传
    $btn.on('click', function () {
        uploader.upload();
    });

    /**
     *  生成进度条封装方法
     * @param file 文件
     * @param percentage 进度值
     * @param id_Prefix id前缀
     * @param titleName 标题名
     */
    function getProgressBar(file, percentage, id_Prefix, titleName) {
        var $li = $('#' + file.id), $percent = $li.find('#' + id_Prefix + '-progress-bar');
        // 避免重复创建
        if (!$percent.length) {
            $percent = $('<div id="' + id_Prefix + '-progress" class="progress progress-striped active">' +
                '<div id="' + id_Prefix + '-progress-bar" class="progress-bar" role="progressbar" style="width: 0%">' +
                '</div>' +
                '</div>'
            ).appendTo($li).find('#' + id_Prefix + '-progress-bar');
        }
        var progressPercentage = parseInt(percentage * 100) + '%';
        $percent.css('width', progressPercentage);
        $percent.html(titleName + ':' + progressPercentage);
    }

    /**
     * 隐藏进度条
     * @param file 文件对象
     * @param id_Prefix id前缀
     */
    function fadeOutProgress(file, id_Prefix) {
        $('#' + file.id).find('#' + id_Prefix + '-progress').fadeOut();
    }
</script>
</html>

后端

application.properties

server.port=8081
#server.servlet.context-path=/recordLog

#数据源必填项
spring.datasource.url=jdbc:mysql://localhost:3306/test?generateSimpleParameterMetadata=true&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#验证连接的有效性
spring.datasource.secondary.test-while-idle=true
#获取连接时候验证,会影响性能
spring.datasource.secondary.test-on-borrow=false
#在连接归还到连接池时是否测试该连接
spring.datasource.secondary.test-on-return=false
spring.datasource.secondary.validation-query=SELECT 1 FROM DUAL
#空闲连接回收的时间间隔,与test-while-idle一起使用,设置5分钟
spring.datasource.secondary.time-between-eviction-runs-millis=300000
#连接池空闲连接的有效时间 ,设置30分钟
spring.datasource.secondary.min-evictable-idle-time-millis=1800000
spring.datasource.secondary.initial-size=5
#指定连接池中最大的活跃连接数.
spring.datasource.secondary.max-active=50
#指定连接池等待连接返回的最大等待时间,毫秒单位.
spring.datasource.secondary.max-wait=60000
#指定必须保持连接的最小值
spring.datasource.secondary.min-idle=5


#Mybatis配置
#mybatis.config-location=classpath:config/sqlMapConfig.xml
mybatis.type-aliases-package=com.example.recordlog.bean
#mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
#引用mybatis-plus-boot-starter写法如下
mybatis-plus.mapper-locations=classpath:mybatis/mapper/*.xml



#redis
spring.redis.host=127.0.0.1
spring.redis.port=6380
spring.redis.database=0
spring.redis.password=123456
spring.redis.timeout=10000
# 设置jedis连接池
spring.redis.jedis.pool.max-active=50
spring.redis.jedis.pool.min-idle=20

#springboot 默认 multipart.max-file-size大小是1M,max-request-size默认大小是10M ,
# 解决方法:在application.properties文件中配置上传信息
#spring.http.multipart.max-file-size=10240MB
#spring.http.multipart.max-request-size=500MB


#注意,此处一定要配置,否则报错
server.tomcat.basedir=./deployer/tomcat





WebConfig静态资源文件配置

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Component
public class WebConfig implements WebMvcConfigurer {


    /**
     * 添加静态资源文件,外部可以直接访问地址
     *
     * @param registry
     */
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/**")
                .addResourceLocations("classpath:/templates/")
                .addResourceLocations("classpath:/static/")
                .addResourceLocations("classpath:/public/");
    }
}

文件上传大小配置

import org.springframework.boot.web.servlet.MultipartConfigFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;

import javax.servlet.MultipartConfigElement;

@Configuration
public class multipartConfig {


    @Bean
    public MultipartConfigElement multipartConfigElement() {
        MultipartConfigFactory factory = new MultipartConfigFactory();
        factory.setMaxRequestSize(DataSize.of(500, DataUnit.MEGABYTES));
        factory.setMaxFileSize(DataSize.of(500, DataUnit.MEGABYTES));
        return factory.createMultipartConfig();
    }
}

controller


import com.example.recordlog.bean.MultipartFileParam;
import com.example.recordlog.tools.JsonResult;
import org.apache.commons.io.FileUtils;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.util.*;

/**
 * @version V1.0
 * @Title: 大文件上传
 * @Description: 断点续传.秒传.分块上传
 * @author:
 */
@RestController
@RequestMapping(value = "/bigfile")
public class BigFileController {

    private Logger logger = LoggerFactory.getLogger(BigFileController.class);

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // @Value("${breakpoint.upload.dir}")
    private String fileStorePath = "D:/新建文件夹/";

    /**
     * @param fileMd5
     * @Title: 判断文件是否上传过,是否存在分片,断点续传
     * @MethodName: checkBigFile
     * @Return com.lovecyy.file.up.example3.vo.JsonResult
     * @Exception
     * @Description: 文件已存在,下标为-1
     * 文件没有上传过,下标为零
     * 文件上传中断过,返回当前已经上传到的下标
     */
    @RequestMapping(value = "/check", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult checkBigFile(String fileMd5) {
        JsonResult jr = new JsonResult();
        // 秒传
        File mergeMd5Dir = new File(fileStorePath + "/" + "merge" + "/" + fileMd5);
        if (mergeMd5Dir.exists()) {
            mergeMd5Dir.mkdirs();
            jr.setResultCode(-1);//文件已存在,下标为-1
            return jr;
        }
        // 读取目录里的所有文件
        File dir = new File(fileStorePath + "/" + fileMd5);
        File[] childs = dir.listFiles();
        if (childs == null) {
            jr.setResultCode(0);//文件没有上传过,下标为零
        } else {
            jr.setResultCode(childs.length - 1);//文件上传中断过,返回当前已经上传到的下标
        }
        return jr;
    }

    /**
     * 上传文件
     *
     * @param param
     * @param request
     * @return
     * @throws Exception
     */
    @RequestMapping(value = "/upload", method = RequestMethod.POST)
    @ResponseBody
    public void filewebUpload(MultipartFileParam param, HttpServletRequest request) {
        boolean isMultipart = ServletFileUpload.isMultipartContent(request);
        // 文件名
        String fileName = param.getName();
        // 文件每次分片的下标
        int chunkIndex = param.getChunk();
        if (isMultipart) {
            File file = new File(fileStorePath + "/" + param.getMd5());
            if (!file.exists()) {
                file.mkdir();
            }
            File chunkFile = new File(
                    fileStorePath + "/" + param.getMd5() + "/" + chunkIndex);
            try {
                FileUtils.copyInputStreamToFile(param.getFile().getInputStream(), chunkFile);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        logger.info("文件-:{}的小标-:{},上传成功", fileName, chunkIndex);
        return;
    }

    /**
     * 分片上传成功之后,合并文件
     *
     * @param request
     * @return
     */
    @RequestMapping(value = "/merge", method = RequestMethod.POST)
    @ResponseBody
    public JsonResult filewebMerge(HttpServletRequest request) {
        FileChannel outChannel = null;
        try {
            String fileName = request.getParameter("fileName");
            String fileMd5 = request.getParameter("fileMd5");
            // 读取目录里的所有文件
            File dir = new File(fileStorePath + "/" + fileMd5);
            File[] childs = dir.listFiles();
            if (Objects.isNull(childs) || childs.length == 0) {
                return null;
            }
            // 转成集合,便于排序
            List<File> fileList = new ArrayList<File>(Arrays.asList(childs));
            Collections.sort(fileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if (Integer.parseInt(o1.getName()) < Integer.parseInt(o2.getName())) {
                        return -1;
                    }
                    return 1;
                }
            });
            // 合并后的文件
            File outputFile = new File(fileStorePath + "/" + "merge" + "/" + fileMd5 + "/" + fileName);
            // 创建文件
            if (!outputFile.exists()) {
                File mergeMd5Dir = new File(fileStorePath + "/" + "merge" + "/" + fileMd5);
                if (!mergeMd5Dir.exists()) {
                    mergeMd5Dir.mkdirs();
                }
                logger.info("创建文件");
                outputFile.createNewFile();
            }
            outChannel = new FileOutputStream(outputFile).getChannel();
            FileChannel inChannel = null;
            try {
                for (File file : fileList) {
                    inChannel = new FileInputStream(file).getChannel();
                    inChannel.transferTo(0, inChannel.size(), outChannel);
                    inChannel.close();
                    // 删除分片
                    file.delete();
                }
            } catch (Exception e) {
                e.printStackTrace();
                //发生异常,文件合并失败 ,删除创建的文件
                outputFile.delete();
                dir.delete();//删除文件夹
            } finally {
                if (inChannel != null) {
                    inChannel.close();
                }
            }
            dir.delete(); //删除分片所在的文件夹
            // FIXME: 数据库操作, 记录文件存档位置
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (outChannel != null) {
                    outChannel.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

MultipartFileParam


import org.springframework.web.multipart.MultipartFile;

public class MultipartFileParam {
    // 用户id
    private String uid;
    //任务ID
    private String id;
    //总分片数量
    private int chunks;
    //当前为第几块分片
    private int chunk;
    //当前分片大小
    private long size = 0L;
    //文件名
    private String name;
    //分片对象
    private MultipartFile file;
    // MD5
    private String md5;

    public String getUid() {
        return uid;
    }

    public void setUid(String uid) {
        this.uid = uid;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public int getChunks() {
        return chunks;
    }

    public void setChunks(int chunks) {
        this.chunks = chunks;
    }

    public int getChunk() {
        return chunk;
    }

    public void setChunk(int chunk) {
        this.chunk = chunk;
    }

    public long getSize() {
        return size;
    }

    public void setSize(long size) {
        this.size = size;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public MultipartFile getFile() {
        return file;
    }

    public void setFile(MultipartFile file) {
        this.file = file;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    @Override
    public String toString() {
        return "MultipartFileParam{" +
                "uid='" + uid + '\'' +
                ", id='" + id + '\'' +
                ", chunks=" + chunks +
                ", chunk=" + chunk +
                ", size=" + size +
                ", name='" + name + '\'' +
                ", file=" + file +
                ", md5='" + md5 + '\'' +
                '}';
    }
}

JsonResult


public class JsonResult<T> {

    private int resultCode;
    private String resultMsg;
    private Object resultData;

    public JsonResult() {
    }

    public JsonResult(int resultCode, String resultMsg, Object resultData) {
        this.resultCode = resultCode;
        this.resultMsg = resultMsg;
        this.resultData = resultData;
    }

    public int getResultCode() {
        return this.resultCode;
    }

    public void setResultCode(int resultCode) {
        this.resultCode = resultCode;
    }

    public String getResultMsg() {
        return this.resultMsg;
    }

    public void setResultMsg(String resultMsg) {
        this.resultMsg = resultMsg;
    }

    public Object getResultData() {
        return this.resultData;
    }

    public void setResultData(Object resultData) {
        this.resultData = resultData;
    }
}

项目目录

页面放在templates文件夹下

webuploader.js和webuploader.css

下载 - Web Uploader

页面效果

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_40428665/article/details/121953399

智能推荐

Rose双机热备两款软件原理介绍以及共享存储双机热备方案和镜像双机热备方案介绍_rose主备切换-程序员宅基地

文章浏览阅读5.1k次,点赞2次,收藏7次。一. RoseHA的工作原理  RoseHA双机系统的两台服务器(主机)都与磁盘阵列(共享存储)系统直接连接,用户的操作系统、应用软件和RoseHA高可用软件分别安装在两台主机上,数据库等共享数据存放在存储系统上,两台主机之间通过私用心跳网络连接。配置好的系统主机开始工作后,RoseHA软件开始监控系统,通过私用网络传递的心跳信息,每台主机上的RoseHA软件都可监控另一台主机的状态。当工作主机..._rose主备切换

Redis缓存数据库SaaS多租户实现方案-程序员宅基地

文章浏览阅读2.7k次。一、前言上2个章节已经实现了mysql和MongoDB的多租户切实现方案,本章将继续学习Redis的多数据源切换。Redis服务器默认有16个database,我们可以将每个租户的数据放到其中一个database中,也可以部署多台Redis服务器,每个租户使用一个Redis服务器,也可以把两者结合起来,Redis服务器部署多台,先在一台的16个Database上放,放满了16个Database然后再往下一台Redis服务器上放。这种方式需要有一个MySQL数据库表存储每台Redis服务器的Databa

win10触摸键盘TabTip软件特性-程序员宅基地

文章浏览阅读2.3k次。win10触摸键盘通过::SendMessage隐藏方式没有效果HWND hWnd = ::FindWindow(L"OSKMainClass", NULL);if ( hWnd ){::SendMessage(hWnd, WM_SYSCOMMAND, SC_MINIMIZE, 0);} win10触摸键盘无法找到状态窗口状态,isWidowsVisible,GetWindowPlacement,GetWindowLong,状态没有变化 ..._tabtip

android实时声音信号波形_【每日一题】(八上)通过波形图比较声音的特性(学苑帮你成长一每日一题精析)9月25日...-程序员宅基地

文章浏览阅读1.5k次。9月25日 通过波形图比较声音的特性中考频度:★★☆☆☆难易程度:★☆☆☆☆(2019·广东初二期末)把频率为256 Hz音叉发出的声音信号输入示波器,示波器展现的波形如图甲所示。若把频率为512 Hz音叉发出的声音信号输入同一设备的示波器,其波形可能图中的__________。【参考答案】丁【试题解析】由题知,原来音叉发出声音的频率为256 Hz,现在音叉发出声音的频率为512 Hz..._规格为256hz音叉声音波形如图所示,将512hz音叉的声音输入同一设置的示波器后,其波

关于Win10安装SQLServer后在程序中不能访问的解决方法_msvcr120 sqlservr windows 无非是访问-程序员宅基地

文章浏览阅读8.1k次。前两天刚在win10中安装完SqlServer 2008 R2 ,安装步骤可以参见这篇文章,装完之后打开 Sql Management Studio,登陆、查询、用Navicat连接都没有问题,于是开始转移以前的项目到本机。然后就出现问题了,在访问项目的时候只要是关于查询数据库的程序全都卡半天,最后报个(org.hibernate.exception.GenericJDBCException: C..._msvcr120 sqlservr windows 无非是访问

Json文件格式化方法_json格式化-程序员宅基地

文章浏览阅读5.3w次,点赞2次,收藏16次。本文详细介绍了 JSON 文件格式化的方法。通过深入探讨,文中提供了多种有效的方式来对 JSON 文件进行格式化,以提高其可读性和可维护性。这些方法涵盖了使用特定工具或的相关技巧和要点。读者可以从中了解到如何快速、准确地对 JSON 文件进行格式化,以便更好地理解和处理其中的数据。_json格式化

随便推点

数学:求欧拉函数算法模板-程序员宅基地

文章浏览阅读499次。数学:求欧拉函数算法模板求欧拉函数求欧拉函数int phi(int x){ int res = x; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) { res = res / i * (i - 1); while (x % i == 0) x /= i; } if (x > 1) res = res / x * .

matplotlib绘图时横纵坐标和图例的字体大小如何设置_matplotlib绘图横纵坐标设置-程序员宅基地

文章浏览阅读8.8k次,点赞3次,收藏8次。横纵坐标字体大小调节:通过fontsize可以进行调节ax1.set_ylabel("AUC",fontsize=20)ax2.set_ylabel("Logloss",fontsize=20)图例字体大小调节:在plt.legend中加一个prop={"size":18,"weight":"black"}即可_matplotlib绘图横纵坐标设置

HTML5特效按钮_html5 特效按钮-程序员宅基地

文章浏览阅读1w次,点赞2次,收藏19次。作为前端开发者,我们肯定都使用过非常多的jQuery插件,毋庸置疑,jQuery非常流行,尤其是结合HTML5和CSS3以后,让这些jQuery插件有了更多地动画效果,更为绚丽多彩。下面分享了一些超炫酷的jQuery/HTML5应用,一起来看看。1、HTML5/CSS3一组可爱的3D按钮这是一款利用HTML5和CSS3制作而成的按钮组合,这款CSS按钮非常具有个性化。该CSS3按钮_html5 特效按钮

树莓派(Raspberry Pi 4)开启和连接蓝牙_树莓派连接蓝牙耳机并使用麦克风-程序员宅基地

文章浏览阅读1.8w次,点赞4次,收藏43次。参考连接: link.1、查看树莓派蓝牙开启状态_树莓派连接蓝牙耳机并使用麦克风

Python3输入输出与字符串格式化_%s.%d' %()-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏12次。介绍了输入(input)、输出(print),及字符串格式化(F-string、format与%)方式_%s.%d' %()

EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码_el 表达式 判断字符串和数字相等-程序员宅基地

文章浏览阅读9.3k次。问题:EL表达式比较字符串或是数字格式的数值是否相等,为true,却不执行为true时的代码。示例:true原因:有可能是test="${ 1 == 1}(这里多个空格)",即大括号与双引号之间多了空格,这个时候,就不会打印true。去掉多余的空格就可以了_el 表达式 判断字符串和数字相等