SpringBoot+vue large file fragment download

Study link

SpringBoot+vue file upload&download&preview&large file segmented upload&file upload progress

Blob & File & FileReader & ArrayBuffer

Vue+SpringBoot implements fragment download of files

Video tag learning & xgplayer video player plays mp4 in segments (Range request interaction process can refer to the screenshots in this)

[java] Java implements fragmented upload and download of large files (springboot+vue3) ( the code has been forked to local )

code

FileController

The code implementation here can be referred to completelyResourceHttpRequestHandler#handleRequest

@RestController
public class FileController {
    
    

    private static final int BUFFER_SIZE = 4 * 1024;

    @RequestMapping(path = "chunkdownload", method = {
    
    RequestMethod.HEAD, RequestMethod.POST})
    public void chunkdownload(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
    

        File file = new File("D:/usr/test/demo.mp4");

        // 文件总大小
        long fileSize = file.length();

        // 设置 Content-Type 和 相关响应头
        // (这里分片下载响应头设置, 其实可以参考ResourceHttpRequestHandler#handleRequest,
        //  和 video标签学习 & xgplayer视频播放器分段播放mp4 - https://blog.csdn.net/qq_16992475/article/details/130945997)
        response.setContentType("application/octect-stream;charset=UTF-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"");
        response.setHeader("Accept-Ranges", "bytes");

        // 检查请求头中是否有Range请求头,
        // (可参考:video标签学习 & xgplayer视频播放器分段播放mp4 - https://blog.csdn.net/qq_16992475/article/details/130945997)
        String rangeHeader = request.getHeader("Range");

        // 没有Range请求头, 则下载整个文件
        if (rangeHeader == null) {
    
    

            response.setHeader("Content-Length", String.valueOf(fileSize));
            InputStream in = new FileInputStream(file);
            OutputStream out = response.getOutputStream();
            // 字节缓冲数组
            byte[] buffer = new byte[BUFFER_SIZE];
            int bytesRead = -1;
            // 读取多少, 写多少, 直到读取完毕为止
            while ((bytesRead = in.read(buffer)) != -1) {
    
    
                out.write(buffer, 0, bytesRead);
            }
            in.close();
            out.close();

        } else {
    
    

            // 分片下载
            // (可参考: 参考ResourceHttpRequestHandler#handleRequest中的做法)

            // 开始索引
            long start = 0;

            // 结束索引
            long end = fileSize - 1;

            // 获取Range请求头的范围, 格式为:Range: bytes=0-8055,
            // (其中可能没有结束位置, 若没有位置, 取文件大小-1)
            String[] range = rangeHeader.split("=")[1].split("-");

            // 如果Range请求头中没有结束位置, 取文件大小-1
            if (range.length == 1) {
    
    

                start = Long.parseLong(range[0]);

                end = fileSize - 1;

            } else {
    
    

                // 解析开始位置 和 结束位置
                start = Long.parseLong(range[0]);

                end = Long.parseLong(range[1]);
            }

            // 此次要写出的数据
            long contentLength = end - start + 1;

            // 返回头里存放每次读取的开始和结束字节
            response.setHeader("Content-Length", String.valueOf(contentLength));
            // 响应状态码206
            response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);

            // Content-Range响应头格式为:Content-Range: bytes 0-8055/9000
            response.setHeader("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);

            InputStream in = new FileInputStream(file);
            OutputStream out = response.getOutputStream();

            // 跳到第start字节
            in.skip(start);

            // 字节缓冲数组
            byte[] buffer = new byte[BUFFER_SIZE];

            // 读取的字节数量
            int bytesRead = -1;

            // 写出的字节数量
            long bytesWritten = 0;


            while ((bytesRead = in.read(buffer)) != -1) {
    
    

                // 如果 已写入的数据 + 当前已读到的数据 超过了 此次要写出的数据, 则只能写入请求范围内的数据
                if (bytesWritten + bytesRead > contentLength) {
    
    
                    out.write(buffer, 0, (int) (contentLength - bytesWritten));
                    break;
                } else {
    
    
                    out.write(buffer, 0, bytesRead);
                    bytesWritten += bytesRead;
                }
            }
            in.close();
            out.close();

        }

    }

}

ChunkDownload.vue

  • First send a head request to get the size of the file
  • Send a post request again to obtain each shard (for the sake of simple understanding, the use of async-await will not be introduced)
  • Combine each shard fetched into a single file
  • (In fact, the following implementation can also be processed using Promise.all() to start multiple downloads at the same time, and then wait for all download tasks to be completed before combining them together)
<template>
    <div class="gap">
        <el-button @click="downloadChunks">分片下载demo.mp4</el-button>
    </div>
</template>

<script>
import axios from 'axios'

export default {
      
      
    name: 'ChunkDownload',
    components: {
      
      
    },
    methods: {
      
      
        downloadChunks() {
      
      
            const chunkdownloadUrl = 'http://localhost:8085/chunkdownload'

            // 分片下载大小 5MB
            const chunkSize = 1024 * 1024 * 5;

            // 文件总大小(需要请求后端获得)
            let fileSize = 0;

            axios
                .head(chunkdownloadUrl)
                .then(res => {
      
      

                    // 定义 存储所有的分片的数组
                    let chunks = [];

                    // 获取文件总大小
                    fileSize = res.headers['content-length']

                    // 计算分片数量
                    const chunksNum = Math.ceil(fileSize / chunkSize)

					// 定义下载文件分片的方法
                    function downloadChunkFile(chunkIdx) {
      
      

                        if (chunkIdx >= chunksNum) {
      
      
                            alert('分片索引不可超过分片数量')
                            return
                        }

                        let start = chunkIdx * chunkSize
                        let end = Math.min(start + chunkSize - 1, fileSize - 1)
                        const range = `bytes=${ 
        start}-${ 
        end}`;

                        axios({
      
      
                            url: chunkdownloadUrl,
                            method: 'post',
                            headers: {
      
      
                                Range: range
                            },
                            responseType: 'arraybuffer'
                        }).then(response => {
      
      
                            chunks.push(response.data)
                            if(chunkIdx == chunksNum - 1) {
      
      
                                // 下载好了
                                console.log(chunks, 'chunks');
                                // 组合chunks到单个文件
                                const blob = new Blob(chunks);
                                console.log(blob, 'blob');
                                const link = document.createElement('a');
                                link.href = window.URL.createObjectURL(blob);
                                link.download = 'demo.mp4';
                                link.click();
                                return
                            } else {
      
      
                                ++chunkIdx
                                downloadChunkFile(chunkIdx)
                            }
                        })
                    }

                    downloadChunkFile(0)

                })


        }
    }
}
</script>

<style>
.gap {
      
      
    padding: 10px;
}
</style>

test

insert image description here

Guess you like

Origin blog.csdn.net/qq_16992475/article/details/132118802