• 大文件分片上传、断点续传、秒传


    小文件上传

    后端:SpringBoot+JDK17
    前端:JavaScript+spark+md5.min.js

    一、依赖

    <parent>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-parentartifactId>
        <version>3.1.2version>
        <relativePath/> 
    parent>
    <groupId>com.examplegroupId>
    <artifactId>uploadDemoartifactId>
    <version>0.0.1-SNAPSHOTversion>
    <name>uploadDemoname>
    <description>uploadDemodescription>
    <properties>
        <java.version>17java.version>
    properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-maven-pluginartifactId>
            plugin>
        plugins>
    build>
    
    • 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

    二、业务代码

    @RestController
    public class UploadController{
    	
    	//上传路径
    	public static final String UPLOAD_PATH = "D:\\upload";
    
    	@RequestMapping("/upload")
    	public ResponseEntity<Map<String,String>> upload(@RequestParam MultipartFile file) throws IOException {
    		
    		File dstFile = new File(UPLOAD_PATH,String.format("%s.%s", UUID.randomUUID(), StringUtils.getFilename(file.getOriginalFilename())));
    		file.transferTo(dstFile);
    		return ResponseEntity.ok(Map.of("path",dstFile.getAbsolutePath()));
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    三、前端显示

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>uploadtitle>
    head>
    <body>
    upload
    
    <form enctype="multipart/form-data">
        <input type="file" name="fileInput" id="fileInput">
        <input type="button" value="上传" onclick="uploadFile()">
    form>
    
    上传结果
    <span id="uploadResult">span>
    
    <script>
        var  uploadResult=document.getElementById("uploadResult")
        function uploadFile() {
            var fileInput = document.getElementById('fileInput');
            var file = fileInput.files[0];
            if (!file) return; // 没有选择文件
    
            var xhr = new XMLHttpRequest();
            // 处理上传进度
            xhr.upload.onprogress = function(event) {
                var percent = 100 * event.loaded / event.total;
                uploadResult.innerHTML='上传进度:' + percent + '%';
            };
            // 当上传完成时调用
            xhr.onload = function() {
                if (xhr.status === 200) {
                    uploadResult.innerHTML='上传成功'+ xhr.responseText;
                }
            }
            xhr.onerror = function() {
                uploadResult.innerHTML='上传失败';
            }
            // 发送请求
            xhr.open('POST', '/upload', true);
            var formData = new FormData();
            formData.append('file', file);
            xhr.send(formData);
        }
    script>
    
    body>
    html>
    
    • 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

    在这里插入图片描述

    【注意事项】

    在上传过程会报文件大小限制错误,主要有三个参数需要设置:

    org.apache.tomcat.util.http.fileupload.impl.SizeLimitExceededException: the request was rejected because its size (46302921) exceeds the configured maximum (10485760)
    
    • 1

    需在springboot的application.properties 或者application.yml中添加:

    max-file-size
    max-request-size

    默认大小分别是1M和10M,因此需要重新设定

    spring.servlet.multipart.max-file-size=1024MB  
    spring.servlet.multipart.max-request-size=1024MB
    
    • 1
    • 2

    如果使用nginx报 413状态码413 Request Entity Too Large,Nginx默认最大上传1MB文件,需要在nginx.conf配置文件中的 http{ }添加配置项:client_max_body_size 1024m
    在这里插入图片描述

    大文件上传

    在这里插入图片描述
    在这里插入图片描述

    一、前端分片

    计算文件MD5值用了spark-md5这个库
    因为文件在传输写入过程中可能会出现错误,导致最终合成的文件可能和原文件不一样,所以要对比一下前端计算的MD5和后端计算的MD5是不是一样,保证上传数据的一致性
    在这里插入图片描述

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>分片上传title>
        <script src="https://cdn.bootcdn.net/ajax/libs/spark-md5/3.0.2/spark-md5.min.js">script>
    head>
    <body>
    分片上传
    
    <form enctype="multipart/form-data">
        <input type="file" name="fileInput" id="fileInput">
        <input type="button" value="计算文件MD5" onclick="calculateFileMD5()">
        <input type="button" value="上传" onclick="uploadFile()">
        <input type="button" value="检测文件完整性" onclick="checkFile()">
    form>
    
    <p>
        文件MD5:
        <span id="fileMd5">span>
    p>
    <p>
        上传结果:
        <span id="uploadResult">span>
    p>
    <p>
        检测文件完整性:
        <span id="checkFileRes">span>
    p>
    
    
    <script>
        //每片的大小
        var chunkSize = 1 * 1024 * 1024;
        var uploadResult = document.getElementById("uploadResult")
        var fileMd5Span = document.getElementById("fileMd5")
        var checkFileRes = document.getElementById("checkFileRes")
        var  fileMd5;
    
    
        function  calculateFileMD5(){
            var fileInput = document.getElementById('fileInput');
            var file = fileInput.files[0];
            getFileMd5(file).then((md5) => {
                console.info(md5)
                fileMd5=md5;
                fileMd5Span.innerHTML=md5;
            })
        }
    
        function uploadFile() {
            var fileInput = document.getElementById('fileInput');
            var file = fileInput.files[0];
            if (!file) return;
            if (!fileMd5) return;
    
    
            //获取到文件
            let fileArr = this.sliceFile(file);
            //保存文件名称
            let fileName = file.name;
    
            fileArr.forEach((e, i) => {
                //创建formdata对象
                let data = new FormData();
                data.append("totalNumber", fileArr.length)
                data.append("chunkSize", chunkSize)
                data.append("chunkNumber", i)
                data.append("md5", fileMd5)
                data.append("file", new File([e],fileName));
                upload(data);
            })
    
    
        }
    
        /**
         * 计算文件md5值
         */
        function getFileMd5(file) {
            return new Promise((resolve, reject) => {
                let fileReader = new FileReader()
                fileReader.onload = function (event) {
                    let fileMd5 = SparkMD5.ArrayBuffer.hash(event.target.result)
                    resolve(fileMd5)
                }
                fileReader.readAsArrayBuffer(file)
            })
        }
    
    
       function upload(data) {
           var xhr = new XMLHttpRequest();
           // 当上传完成时调用
           xhr.onload = function () {
               if (xhr.status === 200) {
                   uploadResult.append( '上传成功分片:' +data.get("chunkNumber")+'\t' ) ;
               }
           }
           xhr.onerror = function () {
               uploadResult.innerHTML = '上传失败';
           }
           // 发送请求
           xhr.open('POST', '/uploadBig', true);
           xhr.send(data);
        }
    
        function checkFile() {
            var xhr = new XMLHttpRequest();
            // 当上传完成时调用
            xhr.onload = function () {
                if (xhr.status === 200) {
                    checkFileRes.innerHTML = '检测文件完整性成功:' + xhr.responseText;
                }
            }
            xhr.onerror = function () {
                checkFileRes.innerHTML = '检测文件完整性失败';
            }
            // 发送请求
            xhr.open('POST', '/checkFile', true);
            let data = new FormData();
            data.append("md5", fileMd5)
            xhr.send(data);
        }
    
        function sliceFile(file) {
            const chunks = [];
            let start = 0;
            let end;
            while (start < file.size) {
                end = Math.min(start + chunkSize, file.size);
                chunks.push(file.slice(start, end));
                start = end;
            }
            return chunks;
        }
    
    script>
    
    body>
    html>
    
    • 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
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141

    二、后端

    两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5
    在这里插入图片描述

    FileChannel fileChannel = randomAccessFile.getChannel();  
    MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, chunkNumber * chunkSize, fileData.length);  
    mappedByteBuffer.put(fileData);
    
    • 1
    • 2
    • 3

    在这里插入图片描述

    @RestController
    public class UploadController {
    
        public static final String UPLOAD_PATH = "D:\\upload\\";
    
        /**
         * @param chunkSize   每个分片大小
         * @param chunkNumber 当前分片
         * @param md5         文件总MD5
         * @param file        当前分片文件数据
         * @return
         * @throws IOException
         */
        @RequestMapping("/uploadBig")
        public ResponseEntity<Map<String, String>> uploadBig(@RequestParam Long chunkSize, @RequestParam Integer totalNumber, @RequestParam Long chunkNumber, @RequestParam String md5, @RequestParam MultipartFile file) throws IOException {
            //文件存放位置
            String dstFile = String.format("%s\\%s\\%s.%s", UPLOAD_PATH, md5, md5, StringUtils.getFilenameExtension(file.getOriginalFilename()));
            //上传分片信息存放位置
            String confFile = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
            //第一次创建分片记录文件
            //创建目录
            File dir = new File(dstFile).getParentFile();
            if (!dir.exists()) {
                dir.mkdir();
                //所有分片状态设置为0
                byte[] bytes = new byte[totalNumber];
                Files.write(Path.of(confFile), bytes);
            }
            //随机分片写入文件
            try (RandomAccessFile randomAccessFile = new RandomAccessFile(dstFile, "rw");
                 RandomAccessFile randomAccessConfFile = new RandomAccessFile(confFile, "rw");
                 InputStream inputStream = file.getInputStream()) {
                //定位到该分片的偏移量(可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的,所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题)
                randomAccessFile.seek(chunkNumber * chunkSize);
                //写入该分片数据大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入,不过使用MappedByteBuffer如果要删除文件可能会存在删除不掉,因为删除了磁盘上的文件,内存的文件还是存在的
                randomAccessFile.write(inputStream.readAllBytes());
                //定位到当前分片状态位置
                randomAccessConfFile.seek(chunkNumber);
                //设置当前分片上传状态为1
                randomAccessConfFile.write(1);
            }
            return ResponseEntity.ok(Map.of("path", dstFile));
        }
    
    
        /**
         * 获取文件分片状态,检测文件MD5合法性
         *
         * @param md5
         * @return
         * @throws Exception
         */
        @RequestMapping("/checkFile")
        public ResponseEntity<Map<String, String>> uploadBig(@RequestParam String md5) throws Exception {
            String uploadPath = String.format("%s\\%s\\%s.conf", UPLOAD_PATH, md5, md5);
            Path path = Path.of(uploadPath);
            //MD5目录不存在文件从未上传过
            if (!Files.exists(path.getParent())) {
                return ResponseEntity.ok(Map.of("msg", "文件未上传"));
            }
            //判断文件是否上传成功
            StringBuilder stringBuilder = new StringBuilder();
            byte[] bytes = Files.readAllBytes(path);
            for (byte b : bytes) {
                stringBuilder.append(String.valueOf(b));
            }
            //所有分片上传完成计算文件MD5
            if (!stringBuilder.toString().contains("0")) {
                File file = new File(String.format("%s\\%s\\", UPLOAD_PATH, md5));
                File[] files = file.listFiles();
                String filePath = "";
                for (File f : files) {
                    //计算文件MD5是否相等
                    if (!f.getName().contains("conf")) {
                        filePath = f.getAbsolutePath();
                        try (InputStream inputStream = new FileInputStream(f)) {
                            String md5pwd = DigestUtils.md5DigestAsHex(inputStream);
                            if (!md5pwd.equalsIgnoreCase(md5)) {
                                return ResponseEntity.ok(Map.of("msg", "文件上传失败"));
                            }
                        }
                    }
                }
                return ResponseEntity.ok(Map.of("path", filePath));
            } else {
                //文件未上传完成,反回每个分片状态,前端将未上传的分片继续上传
                return ResponseEntity.ok(Map.of("chucks", stringBuilder.toString()));
            }
    
        }
        
    }
    
    • 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

    断点续传

    在这里插入图片描述
    在这里插入图片描述
    用/checkFile接口,文件里如果有未完成上传的分片,接口返回chunks字段对就的位置值为0,前端将未上传的分片继续上传,完成后再调用/checkFile就完成了断点续传

    秒传

    只要修改前端代码流程就好了,比如张三上传了一个文件,然后李四又上传了同样内容的文件,同一文件的MD5值可以认为是一样的(虽然会存在不同文件的MD5一样,不过概率很小,可以认为MD5一样文件就是一样)李四调用/checkFile接口后,后端直接返回了李四上传的文件路径,李四就完成了秒传。大部分云盘秒传的思路应该也是这样,只不过计算文件HASH算法更为复杂,返回给用户文件路径也更为安全,要防止被别人算出文件路径了
    在这里插入图片描述
    在这里插入图片描述

  • 相关阅读:
    Boosting Few-Shot Visual Learning with Self-Supervision(代码理解)
    基于空间占有度的主导并置模式挖掘
    基于大模型的剧本创作实践;从互联网转行AIGC经验分享;复旦大学LLM最新教科书(电子版);真格基金被投企业2023秋季联合校招 | ShowMeAI日报
    ITSS认证审核时相关问题与解答
    Ubuntu宝塔显示磁盘被占满的解决方法
    【ARM Coresight 系列文章19.1 -- Cortex-A720 PMU 详细介绍】
    集成内部高端电源开关LTC3637HMSE、LTC3637MPMSE稳压器,TJA1443AT汽车CAN FD收发器。
    WordPress、Typecho 站点如何让 CloudFlare 缓存加速
    如何在微信小程序中实现WebSocket连接
    电脑出现找不到msvcp120.dll无法继续执行代码,不用担心多种方法帮你搞定
  • 原文地址:https://blog.csdn.net/usa_washington/article/details/134409690