实现思路,第一次发送请求时,后端返回文件大小和名称,获取到文件大小后才开始做分片获取信息的流程,最后合并文件
后端代码
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");
}
};
}