springboot2分片上传与极速妙传

分片上传本质就是在前端把一个完整的文件拆分成若干份文件上传,上传完成后,服务器再把上传的若干份文件合并成一个完整的文件,再删除若干份分片文件。

首先导包

<dependencies>
  <!-- 基本依赖 -->
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <!-- mysql -->
  <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <!-- mapper -->
     <dependency>
     <groupId>tk.mybatis</groupId>
     <artifactId>mapper-spring-boot-starter</artifactId>
     <version>2.1.5</version>
  </dependency>
  <!-- fastjson -->
  <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>1.2.79</version>
  </dependency>
   <!-- lombok -->
   <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.22</version>
   </dependency>
</dependencies>

application.yml配置:

server:
  port: 8080
  servlet:
    context-path: /test
spring:
  datasource:
    name: DS #如果存在多个数据源,监控的时候可以通过名字来区分开来。如果没有配置,将会生成一个名字,格式是:"DataSource-" + System.identityHashCode(this)
    type: com.zaxxer.hikari.HikariDataSource
    url: jdbc:mysql://127.0.0.1:3306/upload?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT
    #hikari相关配置
    hikari:
      driver-class-name: com.mysql.cj.jdbc.Driver
      username: root
      password: 123456
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 50MB

# mybatis配置
mybatis:
  type-aliases-package: com.upload.pojo
#  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true
    cache-enabled: true #使全局的映射器启用或禁用缓存。
    lazy-loading-enabled: true #全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。
    aggressive-lazy-loading: true #当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。
    jdbc-type-for-null: null #设置但JDBC类型为空时,某些驱动程序 要指定值,default:OTHER,插入空值时不需要指定类型

logging:
  level:
   com.upload.dao: debug

再数据库建一张表 file

CREATE TABLE `file` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `path` varchar(255) NOT NULL DEFAULT '' COMMENT '文件路径',
  `name` varchar(255) NOT NULL DEFAULT '' COMMENT '文件名称',
  `size` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '文件大小',
  `suffix` varchar(10) NOT NULL DEFAULT '' COMMENT '后缀',
  `type` varchar(10) NOT NULL DEFAULT '' COMMENT '文件类型',
  `share_total` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '文件分片总数',
  `share_index` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '已上传分片索引,默认0',
  `key` varchar(32) NOT NULL DEFAULT '' COMMENT '文件唯一Key',
  `create_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `key` (`key`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='文件分片上传表';

创建对应实体类 FilePojo

package com.upload.pojo;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
import java.util.Date;

/**
 * @author xiaochi
 * @date 2022/3/14 22:52
 * @desc FilePojo
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "file")
public class FilePojo implements Serializable {
    private static final long serialVersionUID = -6334172193008858856L;

    @Id
    private Integer id;
    private String path;
    private String name;
    private Long size;
    private String suffix;
    @Column(name = "`type`")
    private String type;
    private Integer shareTotal;
    private Integer shareIndex;
    @Column(name = "`key`")
    private String key;
    private Date createTime;
    private Date updateTime;
}

接着再创建一个接收前端参数的Vo文件 FileVo

package com.upload.vo;

import com.upload.pojo.FilePojo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.web.multipart.MultipartFile;

/**
 * @author xiaochi
 * @date 2022/3/15 8:38
 * @desc FileVo
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class FileVo extends FilePojo {

    private static final long serialVersionUID = -4528742454491886780L;

    private MultipartFile file;
}

FileVo拥有 FilePojo 的所有属性。接着创建一个 FileDao文件

/**
 * @author xiaochi
 * @date 2022/3/14 22:56
 * @desc FileDao
 */
public interface FileDao extends Mapper<FilePojo>, MySqlMapper<FilePojo> {
}

接着创建控制器 UploadController

package com.upload.controller;

import com.upload.common.R;
import com.upload.dao.FileDao;
import com.upload.pojo.FilePojo;
import com.upload.vo.FileVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import tk.mybatis.mapper.entity.Example;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author xiaochi
 * @date 2022/3/14 22:08
 * @desc UploadController
 */
@Slf4j
@CrossOrigin("*")
@RestController
@RequestMapping("/file")
public class UploadController {

    private static final String FILE_PATH = "d:/upload/";

    @Autowired
    private FileDao fileDao;

    /**
     * 根据文件唯一key判断是否有上传
     * @param key
     * @return
     */
    @GetMapping("/check/{key}")
    public R<FilePojo> check(@PathVariable String key){
        return R.ok(findByKey(key));
    }

    /**
     * 分片上传(表单接收)
     * @param fileVo
     * @return
     * @throws Exception
     */
    @PostMapping(value = "/upload")
//    public R<String> upload(@RequestBody @RequestParam("file") MultipartFile file) throws IOException {
    public R<String> upload(FileVo fileVo) throws Exception {// 表单接收
        MultipartFile file = fileVo.getFile();
        String filename = file.getOriginalFilename();
        String date  = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
        String localPath = new StringBuilder()
                .append(date)
                .append(File.separator)
                .append(fileVo.getName())
                .append(".")
                .append(fileVo.getShareIndex())
                .toString();// 分片文件路径与后缀处理 2022/03/15\13-提交Git仓库.mp4.0 、2022/03/15\13-提交Git仓库.mp4.1、2022/03/15\13-提交Git仓库.mp4.2 .....
        fileVo.setPath(localPath);
        File dest = new File(FILE_PATH + localPath);
        if (!dest.exists()){
            dest.setWritable(true);
            dest.mkdirs();
        }
        file.transferTo(dest);
        FilePojo filePojo = new FilePojo();
        BeanUtils.copyProperties(fileVo,filePojo);
        String path = new StringBuilder()
                .append(date)
                .append(File.separator)
                .append(fileVo.getName())
                .toString();// 数据库保存的最后完整文件的路径与名称, 2022/03/15\13-提交Git仓库.mp4
        filePojo.setPath(path);

        // 查询之前是否有过上传
        Example example = new Example(FilePojo.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("key",filePojo.getKey());
        FilePojo filePojoDb = fileDao.selectOneByExample(example);
        if (filePojoDb == null){
            fileDao.insertSelective(filePojo);
        }else {
            fileDao.updateByExampleSelective(filePojo,example);
        }

        // 判断是否上传玩最后一个分片文件,然后进行合并完整文件并删除所有分片文件
        if (fileVo.getShareIndex().equals(fileVo.getShareTotal()-1)){
            this.merge(filePojo);
        }
        return R.ok(path);
    }

    /**
     * 合并所有分片文件成功后并删除所有分片文件
     * @param filePojo
     * @throws Exception
     */
    private void merge(FilePojo filePojo) throws Exception {
        String path = FILE_PATH + filePojo.getPath();
        FileOutputStream outputStream = new FileOutputStream(new File(path), true);// true表示可追加

        FileInputStream inputStream = null;
        byte[] byt = new byte[10* 1024 * 1024];
        int len;

        try {
            for (int i = 0,leng = filePojo.getShareTotal(); i < leng; i++) {
                // 从第i个分片读取
                inputStream = new FileInputStream(new File(path + "." + i));//
                while ((len = inputStream.read(byt)) != -1){
                    outputStream.write(byt,0,len);
                }
            }
        }catch (IOException e){
            log.error("分片合并异常", e);
        }finally {
            try {
                if (inputStream != null){
                    inputStream.close();
                }
                outputStream.close();
            }catch (Exception e){
                log.error("IO流关闭异常", e);
            }
        }

        // 删除分片文件
        System.gc();
        Thread.sleep(100);

        for (int i = 0,leng = filePojo.getShareTotal(); i < leng; i++) {
            String localPath = path + "." + i;
            File file = new File(localPath);
            if (file.exists()){
                boolean result = file.delete();
                log.info("删除分片文件{},{}", localPath, result ? "成功" : "失败");
            }
        }
    }

    /**
     * 根据文件唯一key查询
     * @param key
     * @return
     */
    private FilePojo findByKey(String key){
        Example example = new Example(FilePojo.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("key",key);
        return fileDao.selectOneByExample(example);
    }
}

然后前端对应的请求代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    上传:
    <input type="file" id="file">
</div>
<button id="btn">点击上传</button>
<script src="https://cdn.bootcdn.net/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
    $(function () {
        $("#btn").click(function () {
            var file = $("#file").prop("files")[0];
            if (file){
                var size = file.size;
                var shareSize = 10 * 1024 * 1024;// 默认分片大小 20M
                var shareIndex = 0;// 默认从第0片开始
                var shareTotal = Math.ceil(size / shareSize);// 计算总分片

                var key = md5(file.name+file.type+file.size);

                var params = {
                    filename:file.name,
                    size:size,
                    suffix:file.name.substring(file.name.lastIndexOf(".")+1),
                    type:file.type,
                    shareTotal:shareTotal,
                    shareIndex:shareIndex,
                    key:key,
                };

                // 先查询是否有过上传,或 上传到哪一个的索引
                $.ajax({
                    type: "GET", // 数据提交类型
                    url: "http://localhost:8080/test/file/check/"+key, // 发送地址
                    dataType:"json",
                    success:function (res) {
                        if (res && res.data){
                            // 存在说明之前上传过,接着判断是否之前上传完成
                            if (res.data.shareIndex === (res.data.shareTotal - 1)){
                                // 相等说之前已经上传完整
                                alert("极速秒传成功");
                            }else{
                                // 不相等说明之前上传中断了,接着再上传
                                upload(params,file,res.data.shareIndex,shareSize);
                            }
                        }else{
                            // 没有穿过就从第0个分片开始上传
                            upload(params,file,shareIndex,shareSize);
                        }
                    }
                });
            }
        })
    });

    /**
     * 上传
     * @param params
     * @param file
     * @param shareIndex
     * @param shareSize
     */
    function upload(params,file,shareIndex,shareSize) {
        var start = shareIndex * shareSize;// 分片起始位置
        var end = Math.min(params.size,start+shareSize);
        var fileShare = file.slice(start,end);// 截取file文件分片,进行上传

        var formData = new FormData();
        formData.append("file",fileShare);
        formData.append("name",params.filename);
        formData.append("size",params.size);
        formData.append("suffix",params.suffix);
        formData.append("type",params.type);
        formData.append("shareTotal",params.shareTotal);
        formData.append("shareIndex",shareIndex);
        formData.append("key",params.key);

        $.ajax({
            type: "POST", // 数据提交类型
            url: "http://localhost:8080/test/file/upload", // 发送地址
            dataType:"json",
            data: formData, //发送数据
            // async: true, // 是否异步
            processData: false,
            contentType: false, // 注意:此处并不是json传输数据,而是表单
            success:function (res) {
                if (shareIndex === (params.shareTotal - 1)){
                    // 分片文件已上传完
                    return;
                }else{
                    // 递归上传分片文件
                    upload(params,file,shareIndex+1,shareSize);
                }
            }
        })
    }
</script>
</body>
</html>
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值