java 上传大文件_大文件分片上传与极速秒传实现

为啥要有断点续传

传统的文件传输方式一旦遇到网络问题或者浏览器做重刷的话,那么对于上传过程是灾难性的。那么为了解决这个问题,就出现了断点续传。在之前已传输的地方再接着传输!今天我们这里就用springboot来实现断点续传!

断点续传的大致流程图

56638b85790fb8e72163cdf1aaaf7e9a.png这里的核心在于如何分片,记录分片,然后再获取分片重新从分片处接着传输!

创建工程

添加依赖

<?xml  version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">    <modelVersion>4.0.0modelVersion>    <parent>        <groupId>org.springframework.bootgroupId>        <artifactId>spring-boot-starter-parentartifactId>        <version>2.3.1.RELEASEversion>        <relativePath/>     parent>    <groupId>com.example.demogroupId>    <artifactId>uploadartifactId>    <version>0.0.1-SNAPSHOTversion>    <name>uploadname>    <description>Demo project for Spring Bootdescription>    <properties>        <java.version>1.8java.version>    properties>    <dependencies>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-thymeleafartifactId>        dependency>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-webartifactId>        dependency>        <dependency>            <groupId>org.springframework.bootgroupId>            <artifactId>spring-boot-starter-testartifactId>            <scope>testscope>            <exclusions>                <exclusion>                    <groupId>org.junit.vintagegroupId>                    <artifactId>junit-vintage-engineartifactId>                exclusion>            exclusions>        dependency>                <dependency>            <groupId>org.projectlombokgroupId>            <artifactId>lombokartifactId>            <optional>trueoptional>        dependency>                <dependency>            <groupId>commons-iogroupId>            <artifactId>commons-ioartifactId>            <version>2.4version>        dependency>        <dependency>            <groupId>commons-fileuploadgroupId>            <artifactId>commons-fileuploadartifactId>            <version>1.3.3version>        dependency>                <dependency>            <groupId>mysqlgroupId>            <artifactId>mysql-connector-javaartifactId>            <scope>runtimescope>            <version>5.1.47version>        dependency>                <dependency>            <groupId>com.baomidougroupId>            <artifactId>mybatis-plus-boot-starterartifactId>            <version>3.3.2version>        dependency>    dependencies>    <build>        <plugins>            <plugin>                <groupId>org.springframework.bootgroupId>                <artifactId>spring-boot-maven-pluginartifactId>            plugin>        plugins>    build>project>

配置文件

#spring.resources.static-locations=classpath:/staticserver.port=8000#文件上传路径file.basepath=E:/BaiduNetdiskDownload/spring.servlet.multipart.max-file-size= 50MBspring.servlet.multipart.max-request-size= 50MBspring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/leotemp?characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghaispring.datasource.username=rootspring.datasource.password=123456# templates文件夹的路径spring.thymeleaf.prefix=classpath:/templates/# templates中的所有文件后缀名,如/templates/main.htmlspring.thymeleaf.suffix=.html

创建数据库

create table file( id INTEGER  primary key AUTO_INCREMENT   comment 'id', path varchar(100) not null COMMENT '相对路径', name varchar(100) COMMENT '文件名', suffix varchar(10) COMMENT '文件后缀', size int COMMENT '文件大小|字节B', created_at BIGINT(20) COMMENT '文件创建时间', updated_at bigint(20) COMMENT '文件修改时间', shard_index int comment '已上传分片', shard_size int COMMENT '分片大小|B', shard_total int COMMENT '分片总数', file_key varchar(100) COMMENT '文件标识')

创建实体类

package com.example.demo.upload.entity;import com.baomidou.mybatisplus.annotation.IdType;import com.baomidou.mybatisplus.annotation.TableId;import com.baomidou.mybatisplus.annotation.TableName;import lombok.Data;@Data@TableName(value = "file")public class FileDTO {    /**    * id    */    @TableId(value = "id", type = IdType.AUTO)    private Integer id;    /**    * 相对路径    */    private String path;    /**    * 文件名    */    private String name;    /**    * 后缀    */    private String suffix;    /**    * 大小|字节B    */    private Integer size;    /**    * 创建时间    */    private Long createdAt;    /**    * 修改时间    */    private Long updatedAt;    /**    * 已上传分片    */    private Integer shardIndex;    /**    * 分片大小|B    */    private Integer shardSize;    /**    * 分片总数    */    private Integer shardTotal;    /**    * 文件标识    */    private String fileKey;}

mapper层

package com.example.demo.upload.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import com.example.demo.upload.entity.FileDTO;import org.springframework.stereotype.Repository;@Repositorypublic interface FileMapper extends BaseMapper<FileDTO> {}

响应工具类

package com.example.demo.upload.utils;import lombok.Data;@Datapublic class Result {    // 成功状态码    public static final int SUCCESS_CODE = 200;    // 请求失败状态码    public static final int FAIL_CODE = 500;    // 查无资源状态码    public static final int NOTF_FOUNT_CODE = 404;    // 无权访问状态码    public static final int ACCESS_DINE_CODE = 403;    /**     * 状态码     */    private int code;    /**     * 提示信息     */    private String msg;    /**     * 数据信息     */    private Object data;    /**     * 请求成功     *     * @return     */    public static Result ok() {        Result r = new Result();        r.setCode(SUCCESS_CODE);        r.setMsg("请求成功!");        r.setData(null);        return r;}    /**     * 请求失败     *     * @return     */    public static Result fail() {        Result r = new Result();        r.setCode(FAIL_CODE);        r.setMsg("请求失败!");        r.setData(null);        return r;}    /**     * 请求成功,自定义信息     *     * @param msg     * @return     */    public static Result ok(String msg) {        Result r = new Result();        r.setCode(SUCCESS_CODE);        r.setMsg(msg);        r.setData(null);        return r;}    /**     * 请求失败,自定义信息     *     * @param msg     * @return     */    public static Result fail(String msg) {        Result r = new Result();        r.setCode(FAIL_CODE);        r.setMsg(msg);        r.setData(null);        return r;}    /**     * 请求成功,自定义信息,自定义数据     *     * @param msg     * @return     */    public static Result ok(String msg, Object data) {        Result r = new Result();        r.setCode(SUCCESS_CODE);        r.setMsg(msg);        r.setData(data);        return r;}    /**     * 请求失败,自定义信息,自定义数据     *     * @param msg     * @return     */    public static Result fail(String msg, Object data) {        Result r = new Result();        r.setCode(FAIL_CODE);        r.setMsg(msg);        r.setData(data);        return r;}    public Result code(Integer code){        this.setCode(code);        return this;}    public Result data(Object data){        this.setData(data);        return this;}    public Result msg(String msg){        this.setMsg(msg);        return this;}}

服务层

package com.example.demo.upload.service;import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import com.example.demo.upload.dao.FileMapper;import com.example.demo.upload.entity.FileDTO;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.List;@Servicepublic class FileService {    @Autowired    private FileMapper fileMapper;    //保存文件    public void save(FileDTO file1){        //根据 数据库的 文件标识来查询 当前视频 是否存在        LambdaQueryWrapper<FileDTO> lambda = new QueryWrapper<FileDTO>().lambda();        lambda.eq(FileDTO::getFileKey,file1.getFileKey());        List<FileDTO> fileDTOS = fileMapper.selectList(lambda);        //如果存在就话就修改        if(fileDTOS.size()!=0){            //根据key来修改            LambdaQueryWrapper<FileDTO> lambda1 = new QueryWrapper<FileDTO>().lambda();            lambda1.eq(FileDTO::getFileKey,file1.getFileKey());            fileMapper.update(file1,lambda1);        }else        {            //不存在就添加            fileMapper.insert(file1);        }    }    //检查文件    public List<FileDTO> check(String key){        LambdaQueryWrapper<FileDTO> lambda = new QueryWrapper<FileDTO>().lambda();        lambda.eq(FileDTO::getFileKey,key);        List<FileDTO> dtos = fileMapper.selectList(lambda);        return dtos;    }}

控制层

package com.example.demo.upload.controller;import com.example.demo.upload.entity.FileDTO;import com.example.demo.upload.service.FileService;import com.example.demo.upload.utils.Result;import lombok.extern.slf4j.Slf4j;import org.apache.commons.io.FilenameUtils;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.ResponseBody;import org.springframework.web.multipart.MultipartFile;import java.io.*;import java.util.List;import java.util.UUID;@Controller@RequestMapping("/file")@Slf4jpublic class FileController {    @Autowired    FileService fileService;    public static final String BUSINESS_NAME = "普通分片上传";    // 设置图片上传路径    @Value("${file.basepath}")    private String basePath;    @RequestMapping("/show")    public String show(){        return "file";    }    /**     * 上传     * @param file     * @param suffix     * @param shardIndex     * @param shardSize     * @param shardTotal     * @param size     * @param key     * @return     * @throws IOException     * @throws InterruptedException     */    @RequestMapping("/upload")    @ResponseBody    public String upload(MultipartFile file,                         String suffix,                         Integer shardIndex,                         Integer shardSize,                         Integer shardTotal,                         Integer size,                         String key) throws IOException, InterruptedException {        log.info("上传文件开始");        //文件的名称        String name = UUID.randomUUID().toString().replaceAll("-", "");        // 获取文件的扩展名        String ext = FilenameUtils.getExtension(file.getOriginalFilename());        //设置图片新的名字        String fileName = new StringBuffer().append(key).append(".").append(suffix).toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4        //这个是分片的名字        String localfileName = new StringBuffer(fileName)                .append(".")                .append(shardIndex)                .toString(); // course\6sfSqfOwzmik4A4icMYuUe.mp4.1        // 以绝对路径保存重名命后的图片        File targeFile=new File(basePath,localfileName);        if(!targeFile.exists()){            targeFile.mkdirs();        }        //上传这个图片        file.transferTo(targeFile);        //数据库持久化这个数据        FileDTO file1=new FileDTO();        file1.setPath(basePath+localfileName);        file1.setSuffix(suffix);        file1.setName(name);        file1.setSuffix(ext);        file1.setSize(size);        file1.setCreatedAt(System.currentTimeMillis());        file1.setUpdatedAt(System.currentTimeMillis());        file1.setShardIndex(shardIndex);        file1.setShardSize(shardSize);        file1.setShardTotal(shardTotal);        file1.setFileKey(key);        //插入到数据库中        //保存的时候 去处理一下 这个逻辑        fileService.save(file1);        //判断当前是不是最后一个分页 如果不是就继续等待其他分页  合并分页        if(shardIndex.equals(shardTotal) ){            file1.setPath(basePath+fileName);            this.merge(file1);        }        return "上传成功";    }    @RequestMapping("/check")    @ResponseBody    public Result check(String key){        List<FileDTO> check = fileService.check(key);        //如果这个key存在的话 那么就获取上一个分片去继续上传        if(check.size()!=0){            return Result.ok("查询成功",check.get(0));        }        return Result.fail("查询失败,可以添加");    }    /**     * @author fengxinglie     * 合并分页     */    private void merge(FileDTO fileDTO) throws FileNotFoundException, InterruptedException {        //合并分片开始        log.info("分片合并开始");        String path = fileDTO.getPath(); //获取到的路径 没有.1 .2 这样的东西        //截取视频所在的路径        path = path.replace(basePath,"");        Integer shardTotal= fileDTO.getShardTotal();        File newFile = new File(basePath + path);        FileOutputStream outputStream = new FileOutputStream(newFile,true); // 文件追加写入        FileInputStream fileInputStream = null; //分片文件        byte[] byt = new byte[10 * 1024 * 1024];        int len;        try {            for (int i = 0; i < shardTotal; i++) {                // 读取第i个分片                fileInputStream = new FileInputStream(new File(basePath + path + "." + (i + 1))); //  course\6sfSqfOwzmik4A4icMYuUe.mp4.1                while ((len = fileInputStream.read(byt)) != -1) {                    outputStream.write(byt, 0, len);                }            }        } catch (IOException e) {            log.error("分片合并异常", e);        } finally {            try {                if (fileInputStream != null) {                    fileInputStream.close();                }                outputStream.close();                log.info("IO流关闭");            } catch (Exception e) {                log.error("IO流关闭", e);            }        }        log.info("分片结束了");        //告诉java虚拟机去回收垃圾 至于什么时候回收  这个取决于 虚拟机的决定        System.gc();        //等待100毫秒 等待垃圾回收去 回收完垃圾        Thread.sleep(100);        log.info("删除分片开始");        for (int i = 0; i < shardTotal; i++) {            String filePath = basePath + path + "." + (i + 1);            File file = new File(filePath);            boolean result = file.delete();            log.info("删除{},{}", filePath, result ? "成功" : "失败");        }        log.info("删除分片结束");    }}

页面

<html lang="en"><head>    <meta charset="UTF-8">    <title>Titletitle>head><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js">script><script type="text/javascript" src="/md5.js">script><script type="text/javascript" src="/tool.js">script><body><table border="1px solid red">    <tr>        <td>文件1td>        <td>            <input name="file" type="file" id="inputfile"/>        td>    tr>    <tr>        <td>td>        <td>            <button onclick="check()">提交button>        td>    tr>table>body><script type="text/javascript">    //上传文件的话  得 单独出来    function test1(shardIndex) {        console.log(shardIndex);        //以formData的方式提交        var fd = new FormData();        //获取表单中的file        var file = $('#inputfile').get(0).files[0];        //文件分片  以20MB为一分片        var shardSize = 20 * 1024 * 1024;        //定义分片索引        var shardIndex = shardIndex;        //定义分片的起始位置        var start = (shardIndex - 1) * shardSize;        //定义分片结束的位置        var end = Math.min(file.size, start + shardSize);        //按大小切割文件段        var fileShard = file.slice(start, end);        //分片的大小        var size = file.size;        //总片数        var shardTotal = Math.ceil(size / shardSize);        //文件的后缀名        var fileName = file.name;        var suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length).toLowerCase();        //把视频的信息存储为一个字符串        var filedetails = file.name + file.size + file.type + file.lastModifiedDate;        //使用当前文件的信息用md5加密生成一个key 这个加密是根据文件的信息来加密的  如果相同的文件 加的密还是一样的        var key = hex_md5(filedetails);        var key10 = parseInt(key, 16);        //把加密的信息 转为一个64位的        var key62 = Tool._10to62(key10);        //前面的参数必须和controller层定义的一样        fd.append('file', fileShard);        fd.append('suffix', suffix);        fd.append('shardIndex', shardIndex);        fd.append('shardSize', shardSize);        fd.append('shardTotal', shardTotal);        fd.append('size', size);        fd.append("key", key62)        $.ajax({            url: "/file/upload",            type: "post",            cache: false,            data: fd,            processData: false,//很重要,告诉jquery不要对form进行处理            contentType: false,//很重要,指定为false才能形成正确的Content-Type            success: function (data) {                //这里应该是一个递归调用                if (shardIndex < shardTotal) {                    var index = shardIndex + 1;                    test1(index);                } else {                    alert(data)                }            },            error: function () {                //请求出错处理            }        })        //发送ajax请求把参数传递到后台里面    }    //判断这个加密文件存在不存在    function check() {        var file = $('#inputfile').get(0).files[0];        //把视频的信息存储为一个字符串        var filedetails = file.name + file.size + file.type + file.lastModifiedDate;        //使用当前文件的信息用md5加密生成一个key 这个加密是根据文件的信息来加密的  如果相同的文件 加的密还是一样的        var key = hex_md5(filedetails);        var key10 = parseInt(key, 16);        //把加密的信息 转为一个64位的        var key62 = Tool._10to62(key10);        //检查这个key存在不存在        $.ajax({            url: "/file/check",            type: "post",            data: {'key': key62},            success: function (data) {                console.log(data);                if (data.code == 500) {                    //这个方法必须抽离出来                    test1(1);                } else {                    if (data.data.shardIndex == data.data.shardTotal) {                        alert("极速上传成功");                    } else {                        //找到这个是第几片 去重新上传                        test1(parseInt(data.data.shardIndex));                    }                }            }        })    }script>html>

这里面注重点就是前端分片的时候,起始片和重点片的计算问题!因为我们知道如果一个文件35M,以20M为一分片的话,他其实是两片的。简单的除法计算1余15!

码云传送门

https://gitee.com/thirtyleo/FragmentUpload.git

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值