阿里OSS 断点续传 【后端 java代码】

断点续传思路

  1. 数据库记录最后传输成功的 那个 序列号 seq
  2. 客户端停止上传了
  3. 客户端重新上传,发送数据包给服务器校验,服务器从数据库中读取 记录,没有读取到记录,就重新上传
  4. 如果发现已经有记录到 seq了,下次 直接从 seq + 1 的数据包开始上传。

断点续传因此 需要数据库辅助。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
package io.github.lyr2000.dissertation.util;

import cn.hutool.crypto.SecureUtil;
import com.aliyun.oss.OSS;
import com.aliyun.oss.model.*;
import io.github.lyr2000.dissertation.components.AliYunOssPropertis;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * @author lyr
 * @description ossUtil
 * @create 2021-11-19 18:54
 */
@Slf4j
public class AliOssUtil {
    public static String generateFileKey(String fileName, String uid) {
        return SecureUtil.sha1(fileName) + uid + FileUtil.getUserFileName(fileName);
    }

    /**
     * 生成文件 key
     *
     * @param fileName
     * @return
     */
    public static String generateFileKey(String fileName, HttpSession session) {
        //将用户的 sessionId 和 文件名拼接一起,隔离不同用户的文件上传,使得全局唯一
        String userId = session.getId();
        // F://app/a.txt
        // aabcccxxxa.txt
        // return SecureUtil.sha1(fileName)+userId + FileUtil.getUserFileName(fileName);
        return generateFileKey(fileName, userId);
    }

    public static String generateUploadId(OSS client, String bucketName, String key) {
        InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest(bucketName, key);
        InitiateMultipartUploadResult result = client.initiateMultipartUpload(request);
        return result.getUploadId();
    }

    /**
     * 合并所有文件分片
     * @param client     上传客户端
     * @param bucketName 桶名字
     * @param key        文件的ID
     * @param uploadId   全局上传 ID ,token
     */
    public static void completeMultipartUpload(OSS client, String bucketName,
                                               String key, String uploadId) {
        ListPartsRequest listPartsRequest = new ListPartsRequest(bucketName, key, uploadId);
        PartListing partListing = client.listParts(listPartsRequest);
        List<PartSummary> parts = partListing.getParts();
        int partCnt = parts.size();

        // String[] list = new String[partCnt];
        List<PartETag> list = new ArrayList<>(partCnt);
        //获取所有已经上传完成的文件分片
        for (PartSummary summary : parts) {
            String eTag = summary.getETag();
            int partNumber = summary.getPartNumber();
            list.add(new PartETag(partNumber, eTag));
        }
        CompleteMultipartUploadRequest end = new CompleteMultipartUploadRequest(bucketName, key, uploadId, list);
        
        //合并所有的 文件分片
        client.completeMultipartUpload(end);
        // res.get

        // new PartETag()
    }

    /**
     *  上传文件分片的封装
     * @param f           文件句柄
     * @param uploadId    上传 的文件ID
     * @param fileKeyName 文件名字
     * @param seqCnt      第几个序列
     * @param oss         上传 client
     * @param prop        配置属性
     */
    public static void uploadSlice(MultipartFile f,
                                   String uploadId,
                                   String fileKeyName,
                                   int seqCnt,
                                   OSS oss, AliYunOssPropertis prop) throws IOException {
        log.info("upload id = {}, fileKeyName = {}, seq = {}", uploadId, fileKeyName, seqCnt);
        UploadPartRequest uploadPartRequest = new UploadPartRequest();
        uploadPartRequest.setKey(fileKeyName);
        uploadPartRequest.setBucketName(prop.getBucketName());
        uploadPartRequest.setUploadId(uploadId);
        uploadPartRequest.setPartSize(f.getSize());
        uploadPartRequest.setPartNumber(seqCnt);
        //设置文件流
        uploadPartRequest.setInputStream(f.getInputStream());
        UploadPartResult uploadPartResult = oss.uploadPart(uploadPartRequest);

        if (uploadPartResult != null) {
            log.info("result = {}", uploadPartResult);
        }

    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package io.github.lyr2000.dissertation.components;

import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/**
 * @author lyr
 * @description 阿里配置
 * @create 2021-11-19 18:01
 */
@Data
@Configuration
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliYunOssPropertis {
    // @Value("${aliyun.oss.accessKey}")
    private String accessKey;

    // @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;

    private String bucketName;

    private String endpoint;


    public OSS getOssClient() {
        return new OSSClientBuilder()
                .build(endpoint, accessKey, accessKeySecret);

    }


}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
package io.github.lyr2000.dissertation.controller.test;

import com.aliyun.oss.OSS;
import io.github.lyr2000.common.dto.R;
import io.github.lyr2000.common.dto.Result;
import io.github.lyr2000.dissertation.components.AliYunOssPropertis;
import io.github.lyr2000.dissertation.util.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpSession;

/**
 * @author lyr
 * @description fileTest
 * @create 2021-11-18 23:44
 */
@Slf4j
@RestController
@RequestMapping("/test/api/file")
@Profile("test")
public class FileTestController {
    @Value("D:/ASUS/Desktop/app/")
    private String FILE_DIR;

    @PostConstruct
    void init() {
        log.info("test 文件上传测试开启,本地测试路径: = {}", FILE_DIR);
    }

    @Autowired
    private AliYunOssPropertis propertis;

    @RequestMapping("/get-uploadId")
    public Result getUploadId(@RequestParam String fileName, HttpSession session) {
        String fileKeyName = AliOssUtil.generateFileKey(fileName, session);
        OSS ossClient = propertis.getOssClient();
        try {

            String uploadId = AliOssUtil.generateUploadId(ossClient, propertis.getBucketName(), fileKeyName);
            //返回生成的 uploadId 
            return R.res()
                    .put("uploadId", uploadId)
                    .end();
        } finally {
            ossClient.shutdown();
        }

    }

    @PostMapping("/oss-slice")
    public Result uploadSliceFile(
            @RequestParam("uploadId") String uploadId,
            @RequestPart("file") MultipartFile file,
            @RequestParam("targetName") String targetName,
            @RequestParam("seq") Integer seq,
            @RequestParam("total") Integer total,
            HttpSession session
    ) {

        String fileKeyName = AliOssUtil.generateFileKey(targetName, session);

        OSS ossClient = propertis.getOssClient();
        try {
            AliOssUtil.uploadSlice(file, uploadId, fileKeyName, seq, ossClient, propertis);
            //这里只能单线程上传, 如果多线程上传 到最后一个 ,最后一个上传完成 ,前面没有上传,就会有问题, 合并的话就要分离到另一个接口
            if (total.equals(seq)) {
                // 传输完成 , 合并所有分片
                log.info("传输完成,合并所有分片文件。");
                AliOssUtil.completeMultipartUpload(ossClient,propertis.getBucketName(),fileKeyName,uploadId);
            }
        } catch (Exception e) {
            log.error("upload error msg = {}",e.getMessage());
            return R.fail();
        }finally {

            ossClient.shutdown();
        }
        return R.ok();
    }

 

}

前端代码示例

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script>
  </head>
  <body>
    <form method="POST" enctype="multipart/form-data">
      上传文件:
      <input
        multiple
        id="file"
        type="file"
        name="file"
        placeholder="上传文件"
      />
      <button id="upload" type="button">上传</button>
    </form>
    <script>
      $("#upload").click(() => {
        getUploadId();
		//upload();
      });
    </script>
	<script>
		function getUploadId() {
		let file = document.getElementById("file").files[0];
			$.ajax({
				url: "/test/api/file/get-uploadId?fileName="+file.name,
				type: "post",
				processData: false,
				contentType: false,
			
          }).success((res)=>{
			console.log(res);
			let uploadId = res.data.uploadId;
			console.log('uploadId',uploadId);
			//
			upload(uploadId)
		  
		  })
		}
	</script>

    <script>
      //设置每个切片大小 , 2M
      let bytesPerPiece = 1024 * 1024 * 2;

      //上传文件函数
      function upload(uploadId) {
        //获取上传文件
        let file = document.getElementById("file").files[0];
        console.log(file);
        let start = 0,
          end,
          index = 0,
          filesize = file.size,
          name = file.name;
        //计算切片总数
        let totalPieces = Math.ceil(filesize / bytesPerPiece);
        console.log('total pieces = ',totalPieces)
        let ucnt = 0; //用于记录第几个数据包
        while (ucnt < totalPieces) {
          end = start + bytesPerPiece;
          if (end > filesize) {
            end = filesize;
          }
          let chunk = file.slice(start, end);
          let formData = new FormData();
          ucnt++;
          let filename = `${name}-no-${ucnt}`;
		  formData.append('uploadId',uploadId);
          formData.append("file", chunk, filename);
          //seq 表示 第几个数据包
          formData.append("seq", ucnt);
          //提交总分片数量
          formData.append("total",totalPieces);

          //最终名字
          formData.append("targetName",name);
          //原生js发请求
          //   let xhr = new XMLHttpRequest();
          //   xhr.onreadystatechange = function(){
          //       if(this.readyState == 4 && this.status == 200){

          //       }
          //   }
          //   xhr.open('post', '/api/upload', true);
          //   xhr.send(formData);
          // 使用jquery,需要将contentType,processData设置为false

          console.log("upload --", formData, ucnt);
          $.ajax({
            url: "/test/api/file/oss-slice",
            type: "post",
            data: formData,
            processData: false,
            contentType: false,
            async: false,
          })
          .success((res) => {
            console.log("ok .. {}", res);
            
          })
          .error((err) => {});
          //传输下一部分  
          start = end + 1

        }
      }
    </script>
  </body>
</html>