分片下载大文件的实现

实现思路,第一次发送请求时,后端返回文件大小和名称,获取到文件大小后才开始做分片获取信息的流程,最后合并文件

后端代码

import com.alibaba.fastjson.JSONObject;
import com.example.demo.config.config.BaseConfig;
import io.swagger.v3.oas.annotations.Operation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;


/**
 * @Author: luojie
 * @Date: 2020/4/26 16:59
 */
@RestController
public class NewFileController {

    private static Logger log = LoggerFactory.getLogger(NewFileController.class);

    @Autowired
    BaseConfig baseConfig;

    @Operation(summary = "分片获取文件二进制信息")
    @GetMapping("/file2")
    public ResponseEntity<byte[]> downloadFile(HttpServletRequest request) throws IOException {
        String rangeHeader = request.getHeader("Range");
        System.out.println("rangeHeader = " + rangeHeader);
        String FILE_PATH = "E:/file/aa.zip";
        File file = new File(FILE_PATH);
        String file_Name = URLEncoder.encode(file.getName(), StandardCharsets.UTF_8);
        //如果没有range信息,则该请求是为了获取到文件的大小和文件名称
        if (rangeHeader == null) {
            // 如果没有Range头部,返回整个文件
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("fileName", file.getName());
            jsonObject.put("fileSize", file.length());
            return ResponseEntity.ok()
                    .contentType(MediaType.APPLICATION_JSON)
                    .body(jsonObject.toJSONString().getBytes());

        }

        long fileLength = file.length();

        // 解析Range头部,这里简化处理,只处理bytes=start-end的形式
        String[] range = rangeHeader.substring("bytes=".length()).split("-");
        long start = Long.parseLong(range[0]);
        long end = range.length > 1 ? Long.parseLong(range[1]) : fileLength - 1;

        if (start >= fileLength) {
            // 如果请求的范围超过文件长度,返回416 Range Not Satisfiable
            return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).build();
        }

        if (end >= fileLength) {
            end = fileLength - 1;
        }

        long contentLength = end - start + 1;

        byte[] bytes = new byte[(int) contentLength];
        try (FileInputStream fis = new FileInputStream(file)) {
            fis.skip(start);
            int bytesRead = fis.read(bytes, 0, (int) contentLength);
            if (bytesRead != -1) {
                HttpHeaders headers = new HttpHeaders();
                headers.add(HttpHeaders.CONTENT_RANGE, String.format("bytes %d-%d/%d", start, end, fileLength));
                headers.add(HttpHeaders.ACCEPT_RANGES, "bytes");
                headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));
                headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
                headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+file_Name+"\"");

                return ResponseEntity.ok()
                        .headers(headers)
                        .body(bytes);
            }
        }

        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
    }
}

前端测试代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文件分片下载</title>
</head>
<body>
    <button id="downloadButton">下载文件</button>

    <script>
        const chunkSize = 1024 * 1024 * 5; // 每个分片大小,5MB

        async function downloadFile(url) {
            // 获取文件总大小和文件名称
            const response = await fetch(url, { method: 'GET' });
			const data = await response.json();
			console.log('data ', data)
            const contentLength = data.fileSize;
			const contentDisposition = response.headers.get('Content-Disposition');
			// 解析文件名
			
			let filename = data.fileName;  
			console.log('filename ', filename)
            const totalSize = parseInt(contentLength, 10);
			console.log('totalSize ', totalSize)
            // 初始化下载范围
            let start = 0;
            let end = chunkSize - 1;
            let chunks = [];

            while (start < totalSize) {
                // 调整最后一个分片的结束位置
                if (end >= totalSize) {
                    end = totalSize - 1;
                }

                // 下载分片
                const chunk = await downloadChunk(url, start, end);
                chunks.push(chunk);

                // 更新下一个分片的开始和结束位置
                start = end + 1;
                end = start + chunkSize - 1;
            }

            // 将所有分片合并成一个Blob对象
            const blob = new Blob(chunks);
            const blobUrl = URL.createObjectURL(blob);

            // 创建下载链接
            const link = document.createElement('a');
            link.href = blobUrl;
            link.download = filename; // 你想要的文件名
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);

            // 释放URL对象
            URL.revokeObjectURL(blobUrl);
        }

        async function downloadChunk(url, start, end) {
            const response = await fetch(url, {
                headers: {
                    'Range': `bytes=${start}-${end}`
                }
            });
            const blob = await response.blob();
            return blob;
        }

        document.getElementById('downloadButton').addEventListener('click', () => {
            
			const fileUrl = 'http://127.0.0.1/gx-apis/file2'; // 替换为实际文件URL
            downloadFile(fileUrl);
        });
    </script>
</body>
</html>

如果出现跨域时,可以在启动类里面添加如下信息

@Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                        .allowedOrigins("*")
                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                        .allowedHeaders("*")
                        .exposedHeaders("Content-Disposition");
            }
        };
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值