项目开发之文件上传 (秒传、断点续传、分片上传)(看这一篇就懂了)

news2024/9/21 2:49:07

目录:

  • 前言
  • 秒传
    • 什么是秒传
    • 核心逻辑
    • 代码实现
  • 小文件上传
    • 什么是小文件上传
    • 核心逻辑
    • 代码实现
  • 分片上传
    • 什么是分片上传
    • 核心逻辑
    • 代码实现
  • 断点续传
    • 什么是断点续传
    • 核心代码实现

在这里插入图片描述

前言

文件上传在项目开发中再常见不过了,大多项目都会涉及到图片、音频、视频、文件的上传,通常简单的一个Form表单就可以上传小文件了,但是遇到大文件时比如1GB以上,或者用户网络比较慢时,简单的文件上传就不能适用了,用户辛苦传了好几十分钟,到最后发现上传失败,这样的系统用户体验是非常差的。
或者用户上传到一半时,把应用退出了,下次进来再次上传,如果让他从头开始传也是不合理的。本文主要通过一个Demo从前端、后端用实战代码演示小文件上传、大文件分片上传、断点续传、秒传的开发原理。

注意事项
在上传过程会报文件大小限制错误
这里需在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

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

秒传

什么是秒传

通俗的说,你把要上传的东西上传,服务器会先做MD5校验,如果服务器上有一样的东西,它就直接给你个新地址,其实你下载的都是服务器上的同一个文件,想要不秒传,其实只要让MD5改变,就是对文件本身做一下修改(改名字不行),例如一个文本文件,你多加几个字,MD5就变了,就不会秒传了.

核心逻辑

a、利用redis的set方法存放文件上传状态,其中key为文件上传的md5,value为是否上传完成的标志位,

b、当标志位true为上传已经完成,此时如果有相同文件上传,则进入秒传逻辑。如果标志位为false,则说明还没上传完成,此时需要在调用set的方法,保存块号文件记录的路径,其中key为上传文件md5加一个固定前缀,value为块号文件记录路径

代码实现

前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>秒传文件上传</title>
</head>
<body>
    <input type="file" id="fileInput" />
    <button onclick="uploadFile()">上传文件</button>

    <script>
        async function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            if (!file) {
                alert('请选择文件');
                return;
            }

            const formData = new FormData();
            formData.append('file', file);

            try {
                const response = await fetch('/upload', {
                    method: 'POST',
                    body: formData
                });
                const result = await response.json();
                if (result.success) {
                    alert('文件上传成功');
                } else {
                    alert('文件上传失败');
                }
            } catch (error) {
                console.error('文件上传失败:', error);
            }
        }
    </script>
</body>
</html>

后端代码实现

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;

@SpringBootApplication
public class FileUploadApplication {
    public static void main(String[] args) {
        SpringApplication.run(FileUploadApplication.class, args);
    }
}

@RestController
public class FileUploadController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @PostMapping("/upload")
    public String uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            Path path = Paths.get("uploads/" + file.getOriginalFilename());
            if (!Files.exists(path.getParent())) {
                Files.createDirectories(path.getParent());
            }
            file.transferTo(path.toFile());

            String fileMd5 = getFileMd5(file.getInputStream());
            String key = "file_" + fileMd5;

            if (redisTemplate.opsForValue().get(key) != null) {
                return "秒传成功";
            } else {
                redisTemplate.opsForValue().set(key, "false");
                redisTemplate.opsForValue().set("block_" + fileMd5, path.toString());
                return "文件正在上传中";
            }
        } catch (IOException | NoSuchAlgorithmException e) {
            return "文件上传失败";
        }
    }

    private String getFileMd5(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
        MessageDigest digest = MessageDigest.getInstance("MD5");
        byte[] hash = digest.digest(inputStream.readAllBytes());
        return String.format("%032x", new BigInteger(1, hash));
    }
}

配置文件(application.properties)

spring.servlet.multipart.max-file-size=2MB
spring.servlet.multipart.max-request-size=10MB

# Redis配置
spring.redis.host=localhost
spring.redis.port=6379

小文件上传

什么是小文件上传

小文件上传通常指的是上传的文件大小相对较小,可以快速上传到服务器,并且上传过程中不需要进行分片处理的文件。

核心逻辑

用户选择文件:用户通过前端界面选择要上传的文件。

表单提交:前端使用multipart/form-data编码类型创建表单,将用户选择的文件附加到表单数据中。

发送请求:前端通过XMLHttpRequest或现代的fetch API发送POST请求到服务器。

处理上传进度:在前端,可以通过监听XMLHttpRequest的onprogress事件来更新上传进度。

服务端接收文件:后端使用Spring Boot的@RestController和@RequestMapping注解来创建处理上传请求的端点。

保存文件:后端接收到文件后,将其保存到服务器的指定目录。

返回响应:上传完成后,后端返回响应,告知前端上传结果。

前端代码解析

代码实现

前端代码实现

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
</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>

后端代码实现

@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()));
    }

}

分片上传

什么是分片上传

分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(我们称之为Part)来进行分别上传,上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件。

当我们前端接收到一个大文件的时候,前端会把这个文件进行切分。告知后端一共切了几个分片以及当前上传的是哪一个分片。

大文件的分片是在前端做的,不是后端!

核心逻辑

在这里插入图片描述
将需要上传的文件按照一定的分割规则,分割成相同大小的数据块;

初始化一个分片上传任务,返回本次分片上传唯一标识;

按照一定的策略(串行或并行)发送各个分片数据块;

发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件。

代码实现

前端上传代码计算文件MD5值用了spark-md5这个库,使用也是比较简单的。这里为什么要计算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>

计算大文件的MD5可能会比较慢,这个可以从流程上进行优化,比如上传使用异步去计算文件MD5、不计算整个文件MD5而是计算每一片的MD5保证每一片数据的一致性。

后端代码实现

后端就两个接口/uploadBig用于每一片文件的上传和/checkFile检测文件的MD5。

这里需要注意的:

  • MD5.conf每一次检测文件不存在里创建个空文件,使用byte[] bytes = new byte[totalNumber];将每一位状态设置为0,从0位天始,第N位表示第N个分片的上传状态,0-未上传 1-已上传,当每将上传成功后使用randomAccessConfFile.seek(chunkNumber)将对就设置为1。

  • randomAccessFile.seek(chunkNumber * chunkSize);可以将光标移到文件指定位置开始写数据,每一个文件每将上传分片编号chunkNumber都是不一样的,所以各自写自己文件块,多线程写同一个文件不会出现线程安全问题。

  • 大文件写入时用RandomAccessFile可能比较慢,可以使用MappedByteBuffer内存映射来加速大文件写入,不过使用MappedByteBuffer如果要删除文件可能会存在删除不掉,因为删除了磁盘上的文件,内存的文件还是存在的。

MappedByteBuffer写文件的用法:

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

大文件上传的完整代码

@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()) {
            //定位到该分片的偏移量
            randomAccessFile.seek(chunkNumber * chunkSize);
            //写入该分片数据
            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()));
        }

    }
    
}

断点续传

有了上面的设计做断点续传就比较简单的,后端代码不需要改变,只要修改前端上传流程就好了:

什么是断点续传

断点续传是一种文件上传技术,它允许在上传过程中如果发生中断,可以从中断的地方继续上传,而不是重新开始整个文件的上传。这在上传大文件时尤其有用,因为它可以显著减少上传时间和提高效率。
核心

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

{
    "chucks": "111111111100000000001111111111111111111111111"
}

核心代码实现

前端代码实现

<!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;
    }
// 其他代码保持不变...

function checkFileAndContinue() {
    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/checkFile', true);
    let data = new FormData();
    data.append("md5", fileMd5);
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            if (xhr.status === 200) {
                const response = JSON.parse(xhr.responseText);
                if (response.msg === '文件未上传') {
                    // 文件未上传,开始上传所有分片
                    uploadFile();
                } else if (response.msg === '文件上传失败') {
                    uploadResult.innerHTML = '文件MD5校验失败';
                } else {
                    // 有分片未上传,继续上传未完成的分片
                    const chunksStatus = response.chucks;
                    let chunksToUpload = [];
                    for (let i = 0; i < chunksStatus.length; i++) {
                        if (chunksStatus.charAt(i) === '0') {
                            chunksToUpload.push(fileArr[i]);
                        }
                    }
                    if (chunksToUpload.length > 0) {
                        uploadChunks(chunksToUpload);
                    } else {
                        uploadResult.innerHTML = '所有分片已上传';
                    }
                }
            } else {
                uploadResult.innerHTML = '检查文件状态失败';
            }
        }
    };
    xhr.send(data);
}

function uploadChunks(chunksToUpload) {
    chunksToUpload.forEach((chunk, i) => {
        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([chunk], fileName));
        upload(data);
    });
}

// 在用户选择文件后,先计算MD5并检查文件上传状态
document.getElementById('fileInput').addEventListener('change', function() {
    var fileInput = document.getElementById('fileInput');
    var file = fileInput.files[0];
    if (file) {
        calculateFileMD5(file).then(md5 => {
            fileMd5 = md5;
            fileMd5Span.innerHTML = md5;
            checkFileAndContinue(); // 检查文件上传状态
        });
    }
});
</script>

</body>
</html>

后端代码

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;

@RestController
public class UploadController {

    public static final String UPLOAD_PATH = "D:\\upload\\";

    /**
     * 上传分片文件
     *
     * @param chunkSize   每个分片大小
     * @param totalNumber 总分片数
     * @param chunkNumber 当前分片编号
     * @param md5         文件MD5
     * @param file        当前分片文件数据
     * @return
     * @throws IOException
     */
    @PostMapping("/uploadBig")
    public ResponseEntity<Map<String, String>> uploadBig(
            @RequestParam("chunkSize") Long chunkSize,
            @RequestParam("totalNumber") Integer totalNumber,
            @RequestParam("chunkNumber") Integer chunkNumber,
            @RequestParam("md5") String md5,
            @RequestParam("file") MultipartFile file) throws IOException {

        // 确保文件目录存在
        String dirPath = String.format("%s%s\\", UPLOAD_PATH, md5);
        File dir = new File(dirPath);
        if (!dir.exists()) {
            dir.mkdirs();
        }

        // 文件存放位置
        String fileName = String.format("%s.part", md5);
        String dstFilePath = String.format("%s%s\\%s", UPLOAD_PATH, md5, fileName);
        // 分片信息存放位置
        String confFilePath = String.format("%s%s\\%s.conf", UPLOAD_PATH, md5, md5);

        // 写入分片数据
        try (FileOutputStream fos = new FileOutputStream(dstFilePath, true);
             FileChannel fileChannel = fos.getChannel();
             InputStream is = file.getInputStream()) {
            long offset = (long) chunkNumber * chunkSize;
            fileChannel.position(offset);
            fileChannel.transferFrom(is.getChannel(), offset, file.getSize());
        }

        // 更新分片状态
        byte[] statusArray = new byte[totalNumber];
        int index = chunkNumber;
        if (!Files.exists(Paths.get(confFilePath))) {
            try (FileOutputStream confFos = new FileOutputStream(confFilePath)) {
                confFos.write(statusArray);
            }
        }
        statusArray[index] = 1;
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(confFilePath, "rw")) {
            randomAccessFile.seek(index);
            randomAccessFile.write(statusArray[index]);
        }

        Map<String, String> response = new HashMap<>();
        response.put("path", dstFilePath);
        return ResponseEntity.ok(response);
    }

    /**
     * 检查文件分片状态,检测文件MD5合法性
     *
     * @param md5 文件MD5
     * @return
     * @throws IOException
     */
    @PostMapping("/checkFile")
    public ResponseEntity<Map<String, Object>> checkFile(@RequestParam("md5") String md5) throws IOException {
        String confFilePath = String.format("%s%s\\%s.conf", UPLOAD_PATH, md5, md5);
        Path confPath = Paths.get(confFilePath);

        if (!Files.exists(confPath.getParent())) {
            return ResponseEntity.ok(Map.of("msg", "文件未上传"));
        }

        boolean isComplete = true;
        try (RandomAccessFile accessFile = new RandomAccessFile(confFilePath, "r")) {
            for (int i = 0; i < accessFile.length(); i++) {
                if (accessFile.readByte() == 0) {
                    isComplete = false;
                    break;
                }
            }
        }

        if (isComplete) {
            // 计算合并后的文件MD5
            String finalFilePath = String.format("%s%s\\%s", UPLOAD_PATH, md5, md5);
            try (InputStream is = new FileInputStream(finalFilePath)) {
                String calculatedMd5 = DigestUtils.md5Hex(IOUtils.toByteArray(is));
                return ResponseEntity.ok(Map.of("msg", "文件上传成功", "md5", calculatedMd5));
            } catch (IOException e) {
                return ResponseEntity.ok(Map.of("msg", "文件合并失败"));
            }
        } else {
            // 未完成上传,返回未上传的分片信息
            try (RandomAccessFile accessFile = new RandomAccessFile(confFilePath, "r")) {
                StringBuilder stringBuilder = new StringBuilder();
                for (int i = 0; i < accessFile.length(); i++) {
                    stringBuilder.append(accessFile.readByte() == 0 ? "0" : "1");
                }
                return ResponseEntity.ok(Map.of("msg", "文件未上传完成", "chunks", stringBuilder.toString()));
            }
        }
    }
}

如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力
在这里插入图片描述如果我的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1935890.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

npm安装依赖包报错,npm ERR! code ENOTFOUND

一、报错现象&#xff1a; npm WARN registry Unexpected warning for https://registry.npmjs.org/: Miscellaneous Warning ETIMEDOUT: request to https://registry.npmjs.org/vue failed, reason: connect ETIMEDOUT 104.16.23.35:443 npm WARN registry Using stale data…

Python | Leetcode Python题解之第235题二叉搜索树的最近公共祖先

题目&#xff1a; 题解&#xff1a; class Solution:def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:ancestor rootwhile True:if p.val < ancestor.val and q.val < ancestor.val:ancestor ancestor.leftelif p.val >…

【力扣】最小栈

&#x1f525;博客主页&#xff1a; 我要成为C领域大神&#x1f3a5;系列专栏&#xff1a;【C核心编程】 【计算机网络】 【Linux编程】 【操作系统】 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 本博客致力于知识分享&#xff0c;与更多的人进行学习交流 设计一个支持 push…

object-C 解答算法:合并两个有序数组(leetCode-88)

合并两个有序数组(leetCode-88) 题目如下图:(也可以到leetCode上看完整题目,题号88) 首先搞懂,什么叫“非递减顺序” 非递减顺序,是指一个序列中的元素从前往后&#xff08;或从左到右&#xff09;保持不减少或相等。 这意味着序列中的元素可以保持相同的值&#xff0c;但不会…

c++ pc输入法例子

1、微软开源demo Windows-classic-samples/Samples/IME at master jiangdon2007/Windows-classic-samples (github.com) 2、打开SampleIME.sln 编译【32位或者64位】 3、将SampleIME.dll 和SampleIMESimplifiedQuanPin.txt 放在同一个目录 4、注册 regsvr32 SampleIME.dl…

spring 5.3.x 、6.1.x、6.0.x 源码本地编译运行

参考大佬文章&#xff0c;完美完成本地idea spring源码编译和demo测试 参考链接&#xff08;spring5.3.x&#xff09; spring5.3.x源码阅读环境搭建 下面是spring6.0.x参考 spring6.0.x jdk调成17 idea 2022.2.4版本本地编译spring源码项目 spring6.0.x 分支 gradle-8…

小程序-4(自定义组件:数据、属性、数据监听器、生命周期函数、插槽、父子通信、behaviors)

目录 1.组件的创建和引用 局部引用组件 全局引用组件 组件和页面的区别 组件样式隔离 ​编辑 组件样式隔离的注意点 修改组件的样式隔离选项 data数据 methods方法 properties属性 data和properties属性的区别 使用setData修改properties的值 2.数据监听器 什么…

stm32入门-----EXTI外部中断(下——实践篇)

目录 前言 一、硬件介绍 1.对射红外线传感器 2.旋转编码器 二、EXTI外部中断C编程 1.开启RCC时钟 2.配置GPIOK口初始化 3.配置AFIO 4.配置EXIT 5.配置NVIC 三、EXIT外部中断项目实操 1.对射红外传感器计数 2.选择编码器计数 前言 本期接着上一期的内容继续学习stm3…

AutoMQ 生态集成 Redpanda Console

通过 Kafka Web UI 更加便利地管理 Kafka/AutoMQ 集群 随着大数据技术的飞速发展&#xff0c;Kafka 作为一种高吞吐量、低延迟的分布式消息系统&#xff0c;已经成为企业实时数据处理的核心组件。然而&#xff0c;Kafka 集群的管理和监控却并非易事。传统的命令行工具和脚本虽…

Java流的概念及API

流的概念 流&#xff08;Stream)的概念代表的是程序中数据的流通&#xff0c;数据流是一串连续不断的数据的集合。在Java程序中&#xff0c;对于数据的输入/输出操作是以流(Stream)的方式进行的。可以把流分为输入流和输出流两种。程序从输入流读取数据&#xff0c;向输出流写入…

Python项目打包与依赖管理指南

在Python开发中&#xff0c;python文件需要在安装有python解释器的计算机的电脑上才能运行&#xff0c;但是在工作时&#xff0c;我们需要给客户介绍演示项目功能时并不一定可以条件安装解释器&#xff0c;而且这样做非常不方便。这时候我们可以打包项目&#xff0c;用于给客户…

《驾驭AI浪潮:伦理挑战与应对策略》

AI发展下的伦理挑战&#xff0c;应当如何应对&#xff1f; 人工智能飞速发展的同时&#xff0c;也逐渐暴露出侵犯数据隐私、制造“信息茧房”等种种伦理风险。随着AI技术在社会各个领域的广泛应用&#xff0c;关于AI伦理和隐私保护问题日趋凸显。尽管国外已出台系列法规来规范…

达梦数据库DM8-索引篇

目录 一、前景二、名词三、语法1、命令方式创建索引1.1 创建索引空间1.2.1 创建普通索引并指定索引数据空间1.2.2 另一种没验证&#xff0c;官方写法1.3 复合索引1.4 唯一索引1.5 位图索引1.6 函数索引 2、创建表时候创建索引3、可视化方式创建索引3.1 打开DM管理工具3.2 找到要…

nginx负载均衡实例

实现效果 浏览器输入地址http://nginx服务器ip(:80)/edu/a.html&#xff0c;实现负债均衡效果&#xff0c;平均分配到 服务器ip:8080和 服务器ip:8081进程中。 准备工作 准备两个tomcat&#xff0c;一个监听在8080端口&#xff0c;一个监听在8081端口。也可以准备多个tomcat。…

如何在电脑上演示手机上APP,远程排查移动端app问题

0序&#xff1a; 对接客户&#xff0c;给领导演示移动端产品&#xff0c;或者远程帮用户排查移动端产品的问题。都需要让别人能够看到自己在操作手机。 会议室可以使用投屏&#xff0c;但需要切换电脑和手机。 排查问题经常都是截图、或者手机上录制视频&#xff0c;十分繁琐…

node-red学习

Node-RED : 起步 1、安装nodejs Node.js — 在任何地方运行 JavaScript 验证 2、更换下载源 // 查看当前下载地址 npm config get registry // 设置淘宝镜像的地址 npm config set registry https://registry.npmmirror.com/ // 查看当前的下载地址 npm config get registry…

抖音矩阵系统源码开发部署流程分享

#短视频矩阵源码 #短视频矩阵 #源码交付 抖音矩阵系统源码开发部署流程如下&#xff1a; 环境配置&#xff1a;首先&#xff0c;需要安装并配置开发环境&#xff0c;包括安装Java JDK、MySQL数据库、Eclipse开发工具等。 数据库设计&#xff1a;根据抖音矩阵系统的需求&…

安全入门day01

一、常用名词 1、前后端 &#xff08;1&#xff09;前端 前端主要负责用户界面的展示和交互。它通常包括HTML、CSS和JavaScript等技术的使用&#xff0c;也可能使用各种前端框架和库&#xff0c;如React、Vue.js、Angular等&#xff0c;来构建更加复杂和动态的用户界面。前端…

Flowable的学习一

今日项目用到了Flowable。简单记录下。 学习中 参考了网上资料&#xff1a; 工作流-Activiti7-基础讲解_activity工作流-CSDN博客 https://juejin.cn/post/7158342433615380517 flowable实战&#xff08;九&#xff09;flowable数据库表中流程实例、活动实例、任务实例三者…

Windows系统如何本地部署Ollama并运行千文qwen大模型详细教程

文章目录 前言1. 运行Ollama2. 安装Open WebUI2.1 在Windows系统安装Docker2.2 使用Docker部署Open WebUI 3. 安装内网穿透工具4. 创建固定公网地址 前言 本文主要介绍如何在Windows系统快速部署Ollama开源大语言模型运行工具&#xff0c;并安装Open WebUI结合cpolar内网穿透软…