Spring Boot 实现文件分片上传和下载

文章目录

    • 一、原理分析
      • 1.1 文件分片
      • 1.2 断点续传和断点下载
      • 1.2 文件分片下载的 HTTP 参数
    • 二、文件上传功能实现
      • 2.1 客户端(前端)
      • 2.2 服务端
    • 三、文件下载功能实现
      • 3.1 客户端(前端)
      • 3.2 服务端
    • 四、功能测试
      • 4.1 文件上传功能测试
      • 4.2 文件下载功能实现
    • 参考资料

完整案例代码:java-demos/spring-boot-demos/spring-boot-file at main · idealzouhu/java-demos (github.com)

一、原理分析

断点上传和下载通常需要支持文件分片。

  • 断点上传:上传大文件时,支持从上一次中断的位置继续上传。
  • 断点下载:下载大文件时,支持从上一次中断的位置继续下载。

1.1 文件分片

文件分片的核心思想是将服务器上的大文件拆分成若干个小文件,等这些小份文件都下载好了之后,最后将小文件合并成一个完整的大文件。

以文件分片上传为例,客户端责任为:

  • 分片: 将文件切割成小片。

  • 记录上传进度: 记录哪些分片已上传、哪些还未上传。

  • 上传分片: 通过 HTTP 请求将每个分片上传到服务端。

服务端责任为:

  • 接收分片: 将每个分片临时存储。
  • 记录已接收分片: 记录分片的索引、大小等信息,以防止重复上传。
  • 文件合并: 在所有分片上传完成后,将它们合并成完整的文件。

1.2 断点续传和断点下载

断点续传依赖于客户端和服务端的进度记录

  • 客户端:在上传分片时记录当前上传到第几块,下一次可以从该分片继续上传。
  • 服务端:通过每个分片的编号记录已接收的分片,检查是否还需要接收未完成的分片。

1.2 文件分片下载的 HTTP 参数

文件分片下载主要依赖于 HTTP 请求头的 Range 参数来实现Range 参数用于HTTP请求中,允许客户端请求特定字节范围的内容,而不是整个资源。这通常用于下载大文件时,使得客户端可以实现分块下载、断点续传等功能。

例如,客户端只希望获取从200字节到400字节的内容。

GET /path/to/file.txt HTTP/1.1
Host: example.com
Range: bytes=200-400

服务器处理请求并返回一个206 Partial Content响应。

HTTP/1.1 206 Partial Content
Content-Range: bytes 200-400/1000
Content-Length: 201
Content-Type: text/plain...(这里是文件的第200到400字节的内容)...

二、文件上传功能实现

在实现文件上传功能中, 客户端负责分片上传并记录上传进度服务端负责接收和合并分片。同时,本文使用分片编号来实现的,并没有使用 range 参数。

2.1 客户端(前端)

创建 fragmentUpload.html 文件,具体代码为:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>File Chunk Upload</title>
</head>
<body>
<h2>File Chunk Upload with Resume</h2>
<input type="file" id="fileInput">
<button onclick="uploadFile()">Upload</button>
<div id="progress"></div><script>const CHUNK_SIZE = 5 * 1024 * 1024;  // 1MBlet uploadedChunks = 0;async function uploadFile() {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) {alert('Please select a file!');return;}let totalChunks = Math.ceil(file.size / CHUNK_SIZE);while (uploadedChunks < totalChunks) {let start = uploadedChunks * CHUNK_SIZE;let end = Math.min(start + CHUNK_SIZE, file.size);let chunk = file.slice(start, end);let formData = new FormData();formData.append('chunk', chunk);formData.append('fileName', file.name);formData.append('chunkNumber', uploadedChunks + 1);formData.append('totalChunks', totalChunks);try {await fetch('/upload-chunk', {method: 'POST',body: formData});uploadedChunks++;document.getElementById('progress').innerText = `Uploaded chunk ${uploadedChunks} of ${totalChunks}`;} catch (error) {alert('Upload failed. Retrying...');break;}}}
</script>
</body>
</html>

前端通过 File.slice() 方法实现文件分片,并逐个上传到服务器。如果上传过程中中断,记录上传进度,并在恢复时从中断处继续上传。

2.2 服务端

服务端接收分片,保存到指定目录,并在所有分片上传完成后合并它们。其中,每个上传的分片通过 chunkNumber 参数被保存到指定的临时目录中。

@RequiredArgsConstructor
@RestController
public class FileChunkUploadController {// 文件临时目录,用于保存上传的分片文件private static final String TEMP_DIR = "D:\\Learning\\temp\\";// 文件上传目录,用于保存合并后的文件private static final String UPLOAD_DIR = "D:\\Learning\\upload\\";/*** 处理单个分片上传请求* <p>*     当文件较大或网络条件不稳定时,客户端可以将文件分割成多个分片分别上传*     这个方法负责接收单个分片,并将其保存到临时目录当所有分片上传完成后,将它们合并成一个完整的文件* </p>** @param chunk 分片文件,包含文件的一部分* @param fileName 原始文件名,用于合并分片时命名* @param chunkNumber 当前分片的编号,从1开始* @param totalChunks 总分片数,用于判断是否所有分片都已上传* @return 分片上传的状态信息* @throws IOException 如果文件操作失败*/@PostMapping("/upload-chunk")public ResponseEntity<String> uploadChunk(@RequestParam("chunk") MultipartFile chunk,@RequestParam("fileName") String fileName,@RequestParam("chunkNumber") int chunkNumber,@RequestParam("totalChunks") int totalChunks) throws IOException {// 保存分片到临时目录File tempFile = new File(TEMP_DIR + fileName + "_" + chunkNumber);chunk.transferTo(tempFile);// 检查是否所有分片都已上传if (isAllChunksUploaded(fileName, totalChunks)) {mergeChunks(fileName, totalChunks);}return ResponseEntity.ok("Chunk " + chunkNumber + " uploaded");}// 判断是否所有分片都上传完毕private boolean isAllChunksUploaded(String fileName, int totalChunks) {for (int i = 1; i <= totalChunks; i++) {File file = new File(TEMP_DIR + fileName + "_" + i);if (!file.exists()) {return false;}}return true;}// 合并所有分片private void mergeChunks(String fileName, int totalChunks) throws IOException {File mergedFile = new File(UPLOAD_DIR + fileName);try (FileOutputStream fos = new FileOutputStream(mergedFile, true)) {for (int i = 1; i <= totalChunks; i++) {File chunkFile = new File(TEMP_DIR + fileName + "_" + i);try (FileInputStream fis = new FileInputStream(chunkFile)) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = fis.read(buffer)) != -1) {fos.write(buffer, 0, bytesRead);}}chunkFile.delete(); // 删除分片}}}
}

三、文件下载功能实现

在实现文件下载功能中,

  • 客户端负责接收分片,记录上传进度,以及最后的合并分片。
  • 服务端负责下载分片。

3.1 客户端(前端)

客户端实现逻辑为:

  1. 存储所有分片:使用 blobParts 数组来存储每个下载的分片的 Blob 对象。

  2. 整合分片:在所有分片下载完成后,使用 new Blob(blobParts) 创建一个完整的 Blob,然后生成一个 URL 并触发下载。

  3. 触发下载:在所有分片下载并整合完后,创建一个下载链接并点击它以触发下载。

创建 fragmentDownload.html 文件,具体代码为:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>File Download with Chunking</title><style>body {font-family: Arial, sans-serif;}#downloadForm {margin: 20px;}#chunkSize {width: 100px;}#progress {margin-top: 20px;}</style>
</head>
<body>
<h1>File Download with Chunking</h1>
<div id="downloadForm"><label for="fileName">File Name:</label><input type="text" id="fileName" placeholder="example.txt" required><br><br><label for="chunkSize">Chunk Size (bytes):</label><input type="number" id="chunkSize" value="1048576" required> <!-- 1 MB --><br><br><button id="downloadButton">Download File</button>
</div>
<div id="progress"><p>Download Progress: <span id="progressText">0</span>%</p>
</div><script>async function getFileLength(fileName) {const response = await fetch(`download-chunk?fileName=${fileName}`, { method: 'HEAD' });const contentLength = response.headers.get('content-length');return parseInt(contentLength, 10);}async function downloadFile(fileName, start, end) {const range = `bytes=${start}-${end}`;const response = await fetch(`download-chunk?fileName=${fileName}`, {method: 'GET',headers: {'Range': range,}});if (response.status === 206) {return await response.blob(); // 返回 Blob 数据} else {throw new Error(`Error: ${response.status}`);}}async function downloadFileInChunks(fileName, chunkSize) {const fileLength = await getFileLength(fileName);let start = 0;let end = Math.min(chunkSize - 1, fileLength - 1);const totalChunks = Math.ceil(fileLength / chunkSize);let downloadedChunks = 0;const blobParts = []; // 存储所有分片的 Blobwhile (start < fileLength) {try {const blob = await downloadFile(fileName, start, end);blobParts.push(blob); // 将分片加入数组downloadedChunks++;const progressPercentage = Math.round((downloadedChunks / totalChunks) * 100);document.getElementById('progressText').innerText = progressPercentage;start += chunkSize;end = Math.min(start + chunkSize - 1, fileLength - 1);} catch (error) {console.error(`Failed to download chunk: ${error}`);break;}}// 所有分片下载完成后,整合成一个 Blobconst finalBlob = new Blob(blobParts);const url = URL.createObjectURL(finalBlob);const a = document.createElement('a');a.href = url;a.download = fileName; // 设置下载文件名document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url); // 释放内存if (start >= fileLength) {alert('Download completed!');}}document.getElementById("downloadButton").onclick = function() {const fileName = document.getElementById("fileName").value;const chunkSize = parseInt(document.getElementById("chunkSize").value);downloadFileInChunks(fileName, chunkSize).catch(console.error);};
</script>
</body>
</html>

在上述代码中,

  • downloadFileInChunks 函数:控制文件下载的分片逻辑,循环调用 downloadFile 函数进行分片下载。
  • downloadFile 函数:执行实际的文件下载请求。其中,HTTP 请求设置为 xhr.open("GET", download-chunk?fileName=${fileName}, true);

3.2 服务端

服务端主要实现根据 range 参数返回对应的文件分片即可。

@RestController
public class FileChunkDownloadController {private static final String FILE_DIRECTORY = "D:\\Program Files\\";// 处理文件下载请求的方法@GetMapping("/download-chunk")public ResponseEntity<StreamingResponseBody> downloadFile(@RequestParam String fileName,@RequestHeader(value = HttpHeaders.RANGE, required = false) String range) throws IOException {// 根据文件名构建文件对象File file = new File(FILE_DIRECTORY, fileName);if (!file.exists()) {return ResponseEntity.status(HttpStatus.NOT_FOUND).build();}// 获取文件长度, 初始化下载的起始和结束位置long fileLength = file.length();long start = 0;long end = fileLength - 1;// 处理 Range 请求if (range != null) {// 解析 Range 请求中的起始和结束位置String[] ranges = range.replace("bytes=", "").split("-");start = Long.parseLong(ranges[0]);if (ranges.length > 1 && !ranges[1].isEmpty()) {end = Long.parseLong(ranges[1]);}}// 确保请求的范围合法if (start > end || start >= fileLength) {// 如果请求范围不合法,返回 416 REQUESTED RANGE NOT SATISFIABLEreturn ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).header(HttpHeaders.CONTENT_RANGE, "bytes */" + fileLength).build();}// 如果结束位置超出文件长度,调整结束位置if (end >= fileLength) {end = fileLength - 1;}// 设置内容长度long contentLength = end - start + 1;// 使用 final 关键字定义的变量final long finalStart = start;final long finalEnd = end;final long finalContentLength = contentLength;// 创建 StreamingResponseBody 对象StreamingResponseBody responseBody = outputStream -> {try (InputStream inputStream = new FileInputStream(file)) {inputStream.skip(finalStart); // 跳过起始位置byte[] buffer = new byte[1024];int bytesRead;long bytesToRead = finalContentLength;while (bytesToRead > 0 && (bytesRead = inputStream.read(buffer, 0, (int) Math.min(buffer.length, bytesToRead))) != -1) {outputStream.write(buffer, 0, bytesRead);bytesToRead -= bytesRead;}} catch (IOException e) {// 打印异常信息e.printStackTrace();}};return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE).header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").header(HttpHeaders.CONTENT_RANGE, "bytes " + finalStart + "-" + finalEnd + "/" + fileLength).header(HttpHeaders.ACCEPT_RANGES, "bytes").header(HttpHeaders.CONTENT_LENGTH, String.valueOf(finalContentLength)).body(responseBody);}
}

四、功能测试

4.1 文件上传功能测试

打开客户端 http://localhost:8080/fragmentUpload.html, 上传文件。

在这里插入图片描述

在测试断点续传的过程中,重启服务端,然后再次点击客户端前端界面的 Upload 按钮。

4.2 文件下载功能实现

打开客户端 http://localhost:8080/fragmentDownload.html, 上传文件 。

不能直接使用 http://localhost:8080/download-chunk?fileName=demo.txt 来访问,会出现问题。

在这里插入图片描述

参考资料

实现大文件的断点下载、分片下载 (qq.com)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【数据结构】-数组

数组 特点&#xff1a; 数组的地址连续&#xff0c;可以通过下标获取数据。 1. 数组扩容 步骤&#xff1a; $1. 创建一个比原来数组更长的新数组 $2. 让原来数组当中的数据依次复制到新数组当中 $3. 让arr指向新数组&#xff0c;原数组空间释放 2. 数组插入 2.1 最后位置…

智慧小区:科技之光点亮幸福家园

智慧社区的未来发展方向与趋势 从智能化管理到便捷化服务&#xff0c;从环保节能到安全监控&#xff0c;智慧社区正以其前瞻性的视野和创新性的技术&#xff0c;引领着未来城市生活的新方向。从智慧社区的基本概念中通过运用现代科技手段&#xff0c;如物联网、云计算、大数据…

0,国产FPGA(紫光同创)-新建PDS工程

国产FPGA正在蓬勃发展&#xff0c;紫光同创FPGA是大家竞赛时经常遇到的一款国产FPGA&#xff0c;本专栏从IP核开始一直到后续图像处理等。 开发板&#xff1a;盘古50K标准板 1&#xff0c;新建PDS工程 点击File&#xff08;1&#xff09;&#xff0c;然后是New Projects&#…

深入解析Sysmon日志:增强网络安全与威胁应对的关键一环

不断演进的网络安全领域中&#xff0c;保持对威胁的及时了解至关重要。Sysmon日志在这方面发挥了至关重要的作用&#xff0c;通过提供有价值的见解&#xff0c;使组织能够加强其安全姿态。Windows在企业环境中是主导的操作系统&#xff0c;因此深入了解Windows事件日志、它们的…

yocto 下基于SDK的 tcpdump 移植

系列文章目录 【1】yocto系统构建 【2】yocto下mosquitto用户名和密码配置 【3】yocto 下基于SDK的 tcpdump 移植 文章目录 系列文章目录前言一、移植tcpdump的意义二、移植步骤1. 准备Yocto环境2.获取源码&#xff0c;配置和编译3.移植到目标设备4.测试tcpdump 总结 前言 tc…

使用python提取日志里面的role_id、vip字段的值,(vip字段可能为空或者缺失,此时需要给默认值0):

日志样例&#xff1a; 1068 1529597015396 g60-database-380.i.nease.net /home/g60/gamedata/log/g60_GameStatistic_20180622.log 380_game02 G60_GameStatistic 1529596878_35 [2018-06-22 00:01:18][MercLevelUp],{"merc_capacity":2739,"cur_level"…

fastGpt

参考本地部署FastGPT使用在线大语言模型 1 rockylinx 1 ollama安装 在rockylinux中安装的&#xff0c;ollama由1.5G&#xff0c;还是比较大&#xff0c;所有采用在windows下下载&#xff0c;然后安装的方式&#xff0c;linux安装 tar -C /usr -xzf ollama-linux-amd64.tgz #…

记一次踩坑ConcurrentModificationException

这段代码中&#xff0c;oDo 是一个 List 类型的对象&#xff0c;subbedList 是从 oDo 中通过 subList(0, 3) 方法获取的子列表。subList 方法返回的是原列表 oDo 的一个视图&#xff0c;而不是一个独立的列表。这意味着对 subbedList 的任何修改都会反映到 oDo 上&#xff0c;反…

实时数据处理:技术支持和优势

在当今快节奏的数字世界中&#xff0c;企业不断寻找在竞争中保持领先地位的方法。批量和近实时的数据处理方法已经无法满足企业对于数据处理速度要求了。因此实时数据处理出现&#xff0c;逐渐帮助企业获取更快速的决策能力。本文&#xff0c;我们将深入研究实时处理&#xff0…

Django 5 增删改查 小练习

1. 用命令创建目录和框架 django-admin startproject myapp cd myapp py manage.py startapp app md templates md static md media 2. Ai 生成代码 一、app/models.py from django.db import modelsclass Product(models.Model):name models.CharField(max_length255, verb…

苏州金龙技术创新赋能旅游新质生产力

2024年10月23日&#xff0c;备受瞩目的“2024第六届旅游出行大会”在云南省丽江市正式开幕。作为客车行业新质生产力标杆客车&#xff0c;苏州金龙在大会期间现场展示了新V系V12商旅版、V11和V8E纯电车型&#xff0c;为旅游出行提供全新升级方案。 其中&#xff0c;全新15座V1…

Atlas800昇腾服务器(型号:3000)—SwinTransformer等NPU推理【图像分类】(九)

服务器配置如下&#xff1a; CPU/NPU&#xff1a;鲲鹏 CPU&#xff08;ARM64&#xff09;A300I pro推理卡 系统&#xff1a;Kylin V10 SP1【下载链接】【安装链接】 驱动与固件版本版本&#xff1a; Ascend-hdk-310p-npu-driver_23.0.1_linux-aarch64.run【下载链接】 Ascend-…

14. NSWindow 窗口与 NSWindowController 窗口控制器

NSWindowController窗口控制器主要用于管理xib/storyboard文件中加载的NSWindow对象&#xff1a;1、创建一个基于xib或storyboard的NSWindowController子类会自动创建一个NSWindow&#xff1b;2、如果手工创建NSWindow对象&#xff0c;则需要维护NSWindowController和NSWindow之…

02 什么是Babel

什么是Babel&#xff1f; Babel 是一个 JavaScript 编译器,提供了JavaScript的编译过程&#xff0c;能够将源代码转换为目标代码。AST -> Transform -> Generate 官网 Babel Babel 查看AST https://astexplorer.net/ Babel所有的包 babel/traverse Babel Babel 是…

【论文阅读笔记】VLP: A Survey on Vision-language Pre-training

目录 前言2 特征提取&#xff08;Feature extraction&#xff09;2.1.1 图象特征提取OD-based Region feature / RoIFreeze the pre-trained object detectorsGrid features&#xff08;网格特征&#xff09;CNN-GFsEnd-to-End Training&#xff08;端到端训练&#xff09;ViT-…

TortoiseSVN小乌龟下载安装(Windows11)

目录 TortoiseSVN 1.14.7工具下载安装 TortoiseSVN 1.14.7 工具 系统&#xff1a;Windows 11 下载 官网&#xff1a;https://tortoisesvn.subversion.org.cn/downloads.html如图选 TortoiseSVN 1.14.7 - 64 位 下载完成 安装 打开 next&#xff0c;next Browse&#xf…

Mac OS 搭建MySQL开发环境

Mac OS 搭建MySQL开发环境 文章目录 Mac OS 搭建MySQL开发环境一、安装Mysql&#xff1a;二、配置环境变量三、安装Navicat 本地环境&#xff1a; Mac OS Sequoia15.0.1&#xff08;M3 Max) 目标状态&#xff1a; 下载安装Mysql&#xff0c;配置相关环境。 一、安装Mysql&…

docker Desktop开启远程访问端口

文章目录 问题解决方法1.首先开启docker Desktop的访问端口2.将本地端口绑定远程访问ip 验证 问题 Windows上部署的docker&#xff0c;没办法通过远程的ip进行访问&#xff0c;实现远程代码的部署。 解决方法 1.首先开启docker Desktop的访问端口 通过开启docker访问端口&am…

Linux文件系统学习(未完)

1. Linux文件系统的特点与类别 1.1 特点 Linux系统中&#xff0c;文件组织在一个统一的树形目录结构中&#xff0c;整个文件系统有一个根“/”&#xff08;文件夹&#xff09;&#xff0c;然后以每个目录&#xff08;文件夹&#xff09;作为分叉&#xff0c;叶子节点作为文件…

Three.js 快速入门构建你的第一个 3D 应用

![ 开发领域&#xff1a;前端开发 | AI 应用 | Web3D | 元宇宙 技术栈&#xff1a;JavaScript、React、Three.js、WebGL、Go 经验经验&#xff1a;6年 前端开发经验&#xff0c;专注于图形渲染和AI技术 开源项目&#xff1a;github 晓智元宇宙、数字孪生引擎、前端面试题 大家好…