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

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>

相关文章:

  • 论数据分流部署模式
  • 组织结构图软件:数据驱动的可视化架构管理工具
  • UE5 读取配置文件
  • ue5.5 landscape paint模式下 layers出不来
  • [论文阅读] 算法 | 布谷鸟算法在声源定位中的应用研究
  • 布尔字段命名陷阱:避免序列化错误的关键
  • 个人网站图片托管存储桶迁移全记录
  • 【Qt】输入类控件 QLineEdit、QTextEdit、QComboBox、QSpinBox、QDateTimeEdit、QDial、QSlider
  • 使用OpenCV和Python进行图像掩膜与直方图分析
  • 高频面试之5Kafka
  • MySQL虚拟列:一个被低估的MySQL特性
  • SDXL 和 SDXL-Turbo 的区别
  • C语言数据结构笔记6:使用宏和指针来设置和操作嵌套在结构体中的联合体数组的特定位
  • SpringBoot学习day1-SpringBoot的简介与搭建
  • SD-WAN 技术如何助力工业物联网(IIoT)数据传输?深度解析传统方案对比与应用实践
  • 解码 K-Means 聚类:开启数据星河的炫酷聚类新纪元
  • 运维之十个问题--2
  • 华为Pura 80系列发布 6499元起
  • 美团完整面经
  • Android学习之Window窗口
  • 深圳品牌网站建设服务/郑州优化网站关键词
  • 做一个网站后期维护需要多少钱/爱站网seo工具
  • tp5手机网站开发/郑州网络推广代理顾问
  • wordpress有主题了怎么操作/济南网站优化公司排名
  • 免费推广网站建设/快速排名刷
  • 运用阿里云怎么做网站/备案查询站长之家