1. 程式人生 > 程式設計 >SpringBoot 中大檔案(分片上傳)斷點續傳與極速秒傳功能的實現

SpringBoot 中大檔案(分片上傳)斷點續傳與極速秒傳功能的實現

1.建立SpringBoot專案

本專案採用springboot + mybatis-plus +jquery +thymeleaf組成

2.專案流程圖

在這裡插入圖片描述

3.在pom中新增以下依賴

<!--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.1</version>
    </dependency>

    <!-- mysql的依賴 -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>

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

4.在application.properties配置檔案中

spring.resources.static-locations=classpath:/static
server.port=8000
    
#設定上傳圖片的路徑
file.basepath=D:/BaiduNetdiskDownload/

# 設定單個檔案大小
spring.servlet.multipart.max-file-size= 50MB
# 設定單次請求檔案的總大小
spring.servlet.multipart.max-request-size= 50MB


##設定要連線的mysql資料庫
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf8&autoReconnect=true&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root

5.在資料庫建立表

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 '檔案標識'
)

6.建立實體類

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;

}

7.建立mapper

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> {
}

8.建立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;
  }

}

9.建立utils

import lombok.Data;

/**
 * 統一返回值
 *
 * @author zhangshuai
 *
 */
@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;
	}

}

10.建立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);
    //上傳這個圖片
    file.transferTo(targeFile);
    //資料庫持久化這個資料
    FileDTO file1=new FileDTO();
    file1.setPath(basePath+localfileName);
    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,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("刪除分片結束");
  }

}

11.建立html頁面

<!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 type="text/javascript" src="/md5.js"></script>
<script type="text/javascript" src="/tool.js"></script>
<script type="text/javascript">
  //上傳檔案的話 得 單獨出來
  function test1(shardIndex) {
    console.log(shardIndex);
    //永安裡from表單提交
    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;
    //定義分片結束的位置 file哪裡來的?
    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,contentType: false,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",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>
<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>
</html>

12.測試

在這裡插入圖片描述

12.2:檢視資料庫

在這裡插入圖片描述

12.3 :已經上傳成功 重新上傳一次

在這裡插入圖片描述

12.4:當我上傳一半的時候 (顯示上傳了一個檔案 然後我們繼續上傳)

在這裡插入圖片描述

12.5:繼續上傳 檢視目錄 發現已經成功了

在這裡插入圖片描述
在這裡插入圖片描述

13.注意事項

13.1:程式中並沒有去判斷 你存在不存在 上傳圖片的地址 所以如果打算使用程式碼 請建立 上傳圖片的目錄 D:/BaiduNetdiskDownload/
13.2 :使用程式碼前請修改配置檔案中的 資料庫連線

14.程式碼 直達地址

到此這篇關於SpringBoot 中大檔案(分片上傳)斷點續傳與極速秒傳功能的實現的文章就介紹到這了,更多相關springboot大檔案斷點續傳秒傳內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!