当前位置: 首页 > web >正文

java大文件分段下载

后端代码

package com.jy.jy.controller;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.*;
import java.io.*;
import java.util.regex.Pattern;
import org.springframework.http.*;
import java.io.IOException;@RestController
@CrossOrigin(origins = "*", maxAge = 3600) // 解决跨域问题
@RequestMapping("/api/common/config/download")
public class FileDownloadController {// 实际生产环境中应通过配置文件设置private static final String DOWNLOAD_DIR = "/path/to/download/files";private static final String FILE_PATH = "D:\\wmxy_repository.rar";// 解析Range请求头的正则表达式private static final Pattern RANGE_PATTERN = Pattern.compile("bytes=(\\d+)-(\\d*)");@GetMapping("/file")public ResponseEntity<byte[]> downloadFile(@RequestParam(value = "name", required = false) String fileName,@RequestHeader(value = "Range", required = false) String rangeHeader) throws IOException {File file = new File(FILE_PATH);if (!file.exists()){return ResponseEntity.notFound().build();}long fileSize = file.length();HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);headers.setContentDisposition(ContentDisposition.attachment().filename(fileName).build());// 关键响应头:声明支持范围请求headers.set("Accept-Ranges", "bytes");try (FileInputStream fis = new FileInputStream(file)){if (rangeHeader == null){// 完整文件下载(200 OK)byte[] content = new byte[(int) fileSize];fis.read(content);headers.setContentLength(fileSize);return new ResponseEntity<>(content, headers, HttpStatus.OK);} else{// 处理分片请求(206 Partial Content)long[] range = parseRange(rangeHeader, fileSize);long start = range[0];long end = range[1];long contentLength = end - start + 1;// 验证范围有效性if (start > end || start >= fileSize){return ResponseEntity.status(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE).header("Content-Range", "bytes */" + fileSize).build();}// 设置分片响应头headers.setContentLength(contentLength);headers.set("Content-Range", "bytes " + start + "-" + end + "/" + fileSize);// 读取分片数据byte[] buffer = new byte[(int) contentLength];fis.skip(start);fis.read(buffer);return new ResponseEntity<>(buffer, headers, HttpStatus.PARTIAL_CONTENT);}}}/*** 解析 Range 请求头,返回 [start, end]*/private long[] parseRange(String rangeHeader, long fileSize) {long start = 0;long end = fileSize - 1;try{String[] parts = rangeHeader.replace("bytes=", "").split("-");start = Long.parseLong(parts[0]);if (parts.length > 1 && !parts[1].isEmpty()){end = Math.min(Long.parseLong(parts[1]), fileSize - 1);}// 防止负数start = Math.max(start, 0);// 防止越界end = Math.min(end, fileSize - 1);} catch (Exception e){// 解析失败时返回完整范围start = 0;end = fileSize - 1;}return new long[]{start, end};}
}

前端代码

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>文件分块下载示例</title><script src="https://cdn.tailwindcss.com"></script><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><script>tailwind.config = {theme: {extend: {colors: {primary: '#165DFF',success: '#00B42A',warning: '#FF7D00',danger: '#F53F3F',},fontFamily: {inter: ['Inter', 'system-ui', 'sans-serif'],},},}}</script><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.download-btn {@apply px-4 py-2 bg-primary text-white rounded-md transition-all duration-300 hover:bg-primary/90 active:scale-95 focus:outline-none focus:ring-2 focus:ring-primary/50;}.control-btn {@apply px-3 py-1.5 rounded-md transition-all duration-300 hover:bg-gray-100 active:scale-95 focus:outline-none;}.progress-bar {@apply h-2 rounded-full bg-gray-200 overflow-hidden;}.progress-value {@apply h-full bg-primary transition-all duration-300 ease-out;}}</style>
</head>
<body class="bg-gray-50 font-inter min-h-screen flex flex-col"><header class="bg-white shadow-sm py-4 px-6"><div class="container mx-auto flex justify-between items-center"><h1 class="text-2xl font-bold text-gray-800">文件分块下载</h1></div></header><main class="flex-grow container mx-auto px-4 py-8"><div class="max-w-3xl mx-auto bg-white rounded-lg shadow-md p-6"><div class="mb-6"><h2 class="text-xl font-semibold text-gray-800 mb-4">选择下载文件</h2><div class="grid grid-cols-1 md:grid-cols-2 gap-4"><div class="flex flex-col"><label class="text-sm font-medium text-gray-700 mb-2">文件列表</label><select id="fileSelect" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"><option value="file1.zip">大型文件1.zip (2.4GB)</option><option value="file2.zip">大型文件2.zip (1.7GB)</option><option value="file3.zip">大型文件3.zip (3.1GB)</option></select></div><div class="flex flex-col"><label class="text-sm font-medium text-gray-700 mb-2">块大小</label><select id="chunkSizeSelect" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"><option value="1048576">1MB</option><option value="5242880" selected>5MB</option><option value="10485760">10MB</option><option value="52428800">50MB</option></select></div></div></div><div id="downloadSection" class="hidden"><div class="flex items-center justify-between mb-3"><h3 class="text-lg font-medium text-gray-800">下载进度</h3><div class="flex space-x-2"><button id="pauseBtn" class="control-btn text-warning hidden"><i class="fa fa-pause mr-1"></i> 暂停</button><button id="resumeBtn" class="control-btn text-primary hidden"><i class="fa fa-play mr-1"></i> 继续</button><button id="cancelBtn" class="control-btn text-danger"><i class="fa fa-times mr-1"></i> 取消</button></div></div><div class="mb-3"><div class="flex justify-between text-sm text-gray-600 mb-1"><span id="fileName">大型文件1.zip</span><span id="progressText">0%</span></div><div class="progress-bar"><div id="progressBar" class="progress-value" style="width: 0%"></div></div></div><div class="grid grid-cols-2 gap-4 text-sm text-gray-600 mb-4"><div><span class="font-medium">已下载:</span> <span id="downloadedSize">0 MB</span></div><div><span class="font-medium">总大小:</span> <span id="totalSize">2.4 GB</span></div><div><span class="font-medium">下载速度:</span> <span id="downloadSpeed">0 KB/s</span></div><div><span class="font-medium">剩余时间:</span> <span id="remainingTime">计算中...</span></div></div><div id="downloadComplete" class="hidden text-success mb-4"><i class="fa fa-check-circle mr-2"></i> 下载完成!</div></div><div class="flex justify-center mt-8"><button id="startDownloadBtn" class="download-btn"><i class="fa fa-download mr-2"></i> 开始下载</button></div></div></main><footer class="bg-gray-800 text-white py-6 px-4"><div class="container mx-auto text-center text-sm"><p>© 文件分块下载示例 | 使用 Tailwind CSS 构建</p></div></footer><script>document.addEventListener('DOMContentLoaded', () => {// DOM 元素const startDownloadBtn = document.getElementById('startDownloadBtn');const pauseBtn = document.getElementById('pauseBtn');const resumeBtn = document.getElementById('resumeBtn');const cancelBtn = document.getElementById('cancelBtn');const downloadSection = document.getElementById('downloadSection');const progressBar = document.getElementById('progressBar');const progressText = document.getElementById('progressText');const downloadedSize = document.getElementById('downloadedSize');const totalSize = document.getElementById('totalSize');const downloadSpeed = document.getElementById('downloadSpeed');const remainingTime = document.getElementById('remainingTime');const downloadComplete = document.getElementById('downloadComplete');const fileSelect = document.getElementById('fileSelect');const chunkSizeSelect = document.getElementById('chunkSizeSelect');// 下载状态let isDownloading = false;let isPaused = false;let downloadedBytes = 0;let totalBytes = 0;let chunks = [];let currentChunk = 0;let startTime = 0;let lastUpdateTime = 0;let timer = null;let abortController = null;// 文件映射(实际项目中应由后端提供)const fileMap = {'file1.zip': { size: 5.51 * 1024 * 1024 * 1024, url: 'http://localhost:89/api/common/config/download/file?name=file1.zip' },'file2.zip': { size: 1.7 * 1024 * 1024 * 1024, url: '/download/file?name=file2.zip' },'file3.zip': { size: 3.1 * 1024 * 1024 * 1024, url: '/download/file?name=file3.zip' },};// 格式化文件大小function formatBytes(bytes, decimals = 2) {if (bytes === 0) return '0 Bytes';const k = 1024;const dm = decimals < 0 ? 0 : decimals;const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];const i = Math.floor(Math.log(bytes) / Math.log(k));return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];}// 格式化剩余时间function formatTime(seconds) {if (seconds < 60) {return `${Math.round(seconds)} 秒`;} else if (seconds < 3600) {const minutes = Math.floor(seconds / 60);const secs = Math.round(seconds % 60);return `${minutes} 分 ${secs} 秒`;} else {const hours = Math.floor(seconds / 3600);const minutes = Math.floor((seconds % 3600) / 60);return `${hours} 时 ${minutes} 分`;}}// 更新下载进度function updateProgress() {const now = Date.now();const elapsed = (now - lastUpdateTime) / 1000; // 秒lastUpdateTime = now;if (elapsed <= 0) return;// 计算下载速度 (KB/s)const speed = ((downloadedBytes - (chunks.length > 0 ? chunks.reduce((sum, chunk) => sum + chunk.size, 0) - chunks[chunks.length - 1].size : 0)) / elapsed) / 1024;// 计算剩余时间const remainingBytes = totalBytes - downloadedBytes;const eta = remainingBytes > 0 ? remainingBytes / (speed * 1024) : 0;// 更新UIconst percent = Math.min(100, Math.round((downloadedBytes / totalBytes) * 100));progressBar.style.width = `${percent}%`;progressText.textContent = `${percent}%`;downloadedSize.textContent = formatBytes(downloadedBytes);downloadSpeed.textContent = `${speed.toFixed(1)} KB/s`;remainingTime.textContent = formatTime(eta);// 下载完成if (downloadedBytes >= totalBytes) {finishDownload();}}// 下载完成处理function finishDownload() {isDownloading = false;isPaused = false;clearInterval(timer);timer = null;// 显示完成消息downloadComplete.classList.remove('hidden');pauseBtn.classList.add('hidden');resumeBtn.classList.add('hidden');// 合并所有块并触发下载const blob = new Blob(chunks.map(chunk => chunk.data), { type: 'application/octet-stream' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = fileSelect.value;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);console.log('下载完成!');}// 下载单个块async function downloadChunk(start, end) {if (!isDownloading || isPaused) return;try {abortController = new AbortController();const signal = abortController.signal;const headers = {'Range': `bytes=${start}-${end}`};const response = await fetch(fileMap[fileSelect.value].url, {method: 'GET',headers,signal});if (!response.ok) {throw new Error(`下载失败: ${response.status} ${response.statusText}`);}const contentRange = response.headers.get('content-range');const contentLength = parseInt(response.headers.get('content-length'), 10);const arrayBuffer = await response.arrayBuffer();// 存储块数据chunks.push({start,end,size: contentLength,data: arrayBuffer});// 更新已下载字节数downloadedBytes += contentLength;// 更新进度updateProgress();// 继续下载下一个块currentChunk++;if (currentChunk < chunksInfo.length) {await downloadChunk(chunksInfo[currentChunk].start, chunksInfo[currentChunk].end);}} catch (error) {if (error.name === 'AbortError') {console.log('下载已取消');} else {console.error('下载出错:', error);alert(`下载出错: ${error.message}`);}isDownloading = false;}}// 开始下载async function startDownload() {const selectedFile = fileSelect.value;const chunkSize = parseInt(chunkSizeSelect.value, 10);// 重置状态isDownloading = true;isPaused = false;downloadedBytes = 0;chunks = [];currentChunk = 0;downloadComplete.classList.add('hidden');// 获取文件总大小totalBytes = fileMap[selectedFile].size;totalSize.textContent = formatBytes(totalBytes);// 显示下载区域downloadSection.classList.remove('hidden');pauseBtn.classList.remove('hidden');resumeBtn.classList.add('hidden');// 计算块信息const fileSize = totalBytes;const numChunks = Math.ceil(fileSize / chunkSize);chunksInfo = [];for (let i = 0; i < numChunks; i++) {const start = i * chunkSize;const end = Math.min((i + 1) * chunkSize - 1, fileSize - 1);chunksInfo.push({ start, end });}// 开始计时startTime = Date.now();lastUpdateTime = startTime;// 启动进度更新计时器clearInterval(timer);timer = setInterval(updateProgress, 1000);// 开始下载第一个块await downloadChunk(chunksInfo[0].start, chunksInfo[0].end);}// 暂停下载function pauseDownload() {isPaused = true;if (abortController) {abortController.abort();}pauseBtn.classList.add('hidden');resumeBtn.classList.remove('hidden');}// 恢复下载async function resumeDownload() {isPaused = false;pauseBtn.classList.remove('hidden');resumeBtn.classList.add('hidden');// 继续下载当前块if (currentChunk < chunksInfo.length) {await downloadChunk(chunksInfo[currentChunk].start, chunksInfo[currentChunk].end);}}// 取消下载function cancelDownload() {isDownloading = false;isPaused = false;if (abortController) {abortController.abort();}clearInterval(timer);timer = null;// 重置UIdownloadSection.classList.add('hidden');pauseBtn.classList.add('hidden');resumeBtn.classList.add('hidden');progressBar.style.width = '0%';progressText.textContent = '0%';downloadedSize.textContent = '0 MB';downloadSpeed.textContent = '0 KB/s';remainingTime.textContent = '计算中...';// 清除块数据chunks = [];downloadedBytes = 0;currentChunk = 0;console.log('下载已取消');}// 事件监听startDownloadBtn.addEventListener('click', startDownload);pauseBtn.addEventListener('click', pauseDownload);resumeBtn.addEventListener('click', resumeDownload);cancelBtn.addEventListener('click', cancelDownload);});</script>
</body>
</html>
http://www.xdnf.cn/news/13585.html

相关文章:

  • (ML-Agents) 是一个开源项目,它使游戏和模拟能够作为使用深度强化学习和模仿学习训练智能代理的环境
  • Java SE - 类和对象入门指南
  • MCP 协议系列序言篇:开启 AI 应用融合新时代的钥匙
  • 爬取汽车之家评论并利用NLP进行关键词提取
  • 2025.6.11总结
  • RuoYi 前后端分离项目 Linux 部署全指南
  • 第四章无线通信网
  • 安卓15开机启动Fallbackhome去除--成果展示
  • 看板中如何管理技术债务
  • 智慧厕所系统:革新公共卫生设施的新势力
  • 以 OCP 认证培训为翼,翱翔数据库广阔天空
  • 基础篇:5. HTTP/2 协议深度解析
  • 青藏高原地区多源融合降水数据(1998-2017)
  • C#使用MindFusion.Diagramming框架绘制流程图(3):加权图的最短路径算法
  • Web APIS Day03
  • 全连接层和卷积层
  • 辗转相除法(求最大公约数)
  • Boost.Interprocess 介绍与使用
  • 2025年高考志愿填报指导资料
  • shap可解释恶意流量检测
  • Zab协议剖析:崩溃恢复与顺序原子广播
  • JS手写代码篇---手写深拷贝
  • 万字深度解析注意力机制全景:掌握Transformer核心驱动力​
  • PHP性能提升方案
  • Redis的主从复制底层实现
  • 数组方法_push()/pop()/数组方法_shift()/unshift()
  • Springboot中 MyBatis-Flex TableDef 的使用
  • 常见的CAN总线协议面试题
  • 一套基于Apple watch电话手表包含150个覆盖商务、健康、爱好、定位、时钟、挂件的移动端UI界面的psd
  • 多项式求和