后端代码
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";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){byte[] content = new byte[(int) fileSize];fis.read(content);headers.setContentLength(fileSize);return new ResponseEntity<>(content, headers, HttpStatus.OK);} else{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);}}}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', () => {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;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;const 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;downloadSection.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>