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

为啥要有断点续传

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

断点续传的大致流程图

在这里插入图片描述
这里的核心在于如何分片,记录分片,然后再获取分片重新从分片处接着传输!

创建工程

添加依赖

<?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.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example.demo</groupId>
    <artifactId>upload</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>upload</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!--lombok依赖-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!--文件上传依赖-->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.4</version>
        </dependency>
        <dependency>
            <groupId>commons-fileupload</groupId>
            <artifactId>commons-fileupload</artifactId>
            <version>1.3.3</version>
        </dependency>

        <!-- mysql的依赖 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
            <version>5.1.47</version>
        </dependency>

        <!-- mybatis-plus依赖 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.2</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

配置文件

#spring.resources.static-locations=classpath:/static
server.port=8000

#文件上传路径
file.basepath=E:/BaiduNetdiskDownload/

spring.servlet.multipart.max-file-size= 50MB
spring.servlet.multipart.max-request-size= 50MB

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/leotemp?characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123456

# templates文件夹的路径
spring.thymeleaf.prefix=classpath:/templates/
# templates中的所有文件后缀名,如/templates/main.html
spring.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;

@Repository
public interface FileMapper extends BaseMapper<FileDTO> {
}

响应工具类

package com.example.demo.upload.utils;

import lombok.Data;

@Data
public 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;

@Service
public 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")
@Slf4j
public 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("删除分片结束");
    }
}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<!--<script src="jquery-3.3.1.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>文件1</td>
        <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

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值