springboot2分片上传与极速妙传

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

首先导包


  
  
     org.springframework.boot
     spring-boot-starter-web
  
  
  
     mysql
     mysql-connector-java
  
  
     
     tk.mybatis
     mapper-spring-boot-starter
     2.1.5
  
  
  
     com.alibaba
     fastjson
     1.2.79
  
   
   
      org.projectlombok
      lombok
      1.18.22
   

application.yml配置:

server:
  port: 8080
  servlet:
    context-path: /test
  tomcat:
    max-http-form-post-size: -1
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, MySqlMapper {
}

接着创建控制器 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.BufferedInputStream;
import java.io.BufferedOutputStream;
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 check(@PathVariable String key){
        return R.ok(findByKey(key));
    }

    /**
     * 分片上传(表单接收)
     * @param fileVo
     * @return
     * @throws Exception
     */
    @PostMapping(value = "/upload")
//    public R upload(@RequestBody @RequestParam("file") MultipartFile file) throws IOException {
    public R 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.getParentFile().exists()){
            dest.getParentFile().setWritable(true);
            dest.getParentFile().mkdirs();// 不加 getParentFile() 创建的是文件夹,不是文件
        }
        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表示可追加
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(new File(path), true));

        //FileInputStream inputStream = null;
        BufferedInputStream bis = 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);
                }*/
                bis = new BufferedInputStream(new FileInputStream(new File(path + "." + i)));
                while ((len = bis.read(byt))!= -1){
                    bos.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);
            }*/
            bos.flush();
            bos.close();
            if (bis != null){
                bis.close();
            }
        }

        // 删除分片文件
        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);
    }
}

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




    
    Title


上传:

到此完成,但是这里后台接口没有做文件完整性验证,如要进行完整性验证,请看下面

文件完整性验证上传

要进行文件完整性验证就不能直接 MultipartFile接收了,要在前端吧文件转成 Base64 进行上传了,然后后台接收后解析成 MultipartFile

首先,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 String file;// base64文件字符串
    private String encryFile;// 前端进行md5加密后的符
}

接着新建一个文件 Base64DecodeMultipartFile 用来将接收到的Base64字符串转成 MultipartFile

package com.upload.util;

import org.springframework.web.multipart.MultipartFile;
import sun.misc.BASE64Decoder;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;

/** base64转为multipartFile
 * @author xiaochi
 * @date 2022/4/11 15:30
 * @desc Base64DecodeMultipartFile
 */
public class Base64DecodeMultipartFile implements MultipartFile {
    private final byte[] imgContent;

    private final String header;

    public Base64DecodeMultipartFile(byte[] imgContent, String header) {
        this.imgContent = imgContent;
        this.header = header.split(";")[0];
    }

    @Override
    public String getName() {
        return System.currentTimeMillis() + Math.random() + "." + header.split("/")[1];
    }

    @Override
    public String getOriginalFilename() {
        return System.currentTimeMillis() + (int) Math.random() * 10000 + "." + header.split("/")[1];
    }

    @Override
    public String getContentType() {
        return header.split(":")[1];
    }

    @Override
    public boolean isEmpty() {
        return imgContent == null || imgContent.length == 0;
    }

    @Override
    public long getSize() {
        return imgContent.length;
    }

    @Override
    public byte[] getBytes() throws IOException {
        return imgContent;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(imgContent);
    }

    @Override
    public void transferTo(File dest) throws IOException, IllegalStateException {
        new FileOutputStream(dest).write(imgContent);
    }

    /**
     *  * base64转multipartFile
     *  * @param base64
     *  * @return
     */
    public static MultipartFile base64Convert(String base64) {
        String[] baseStrs = base64.split(",");
        BASE64Decoder decoder = new BASE64Decoder();
        byte[] b = new byte[0];
        try {
            b = decoder.decodeBuffer(baseStrs[1]);
        } catch (IOException e) {
            e.printStackTrace();
        }
        for (int i = 0; i < b.length; ++i) {
            if (b[i] < 0) {
                b[i] += 256;
            }
        }
        return new Base64DecodeMultipartFile(b, baseStrs[0]);
    }
}

接着修改上传文件的接口控制器方法 UploadController的上传方法 upload

/**
 * 分片上传(表单接收),且进行文件完整性验证
 * @param fileVo
 * @return
 * @throws Exception
 */
@PostMapping("/upload")
public R upload(FileVo fileVo) throws Exception {
    MultipartFile file = Base64DecodeMultipartFile.base64Convert(fileVo.getFile());// 将 Base64 字符串解析成 MultipartFile
    // 验证文件完整性
    if (!Objects.equals(fileVo.getEncryFile(),DigestUtils.md5DigestAsHex(fileVo.getFile().getBytes()))){
        return R.error("上传文件已被损坏");
    }
    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.getParentFile().exists()){
        dest.getParentFile().setWritable(true);
        dest.getParentFile().mkdirs();// 不加 getParentFile() 创建的是文件夹,不是文件
    }
    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);
}

接着修改对应的前端代码




    
    Title


上传:

然后运行上传完整,ok,到此结束。

你可能感兴趣的:(springboot2分片上传与极速妙传)