怎么设计一个大文件上传的功能?
1 个回答
想多了全是问题,想开了全是答案。Rose的肉丝儿大文件上传功能需要解决网络不稳定、断点续传等问题。以下是详细的设计步骤和方案。 ### 1. 计算文件 Hash 首先,前端计算大文件的 Hash 值,用于以下用途: - 判断文件是否已经存在服务器上。 - 作为上传文件的唯一标识。 由于大文件的完整 Hash 计算可能耗时较长,通常采用抽样计算 Hash 值的方法。具体方法如下: #### 影分身 Hash - 将文件切分成 2MB 的分片。 - 取首文件、中间每个分片的首、中、尾各 2 个字节,以及尾文件,合并这些片段,计算合并文件的 MD5 值,作为文件的 Hash 值。 通过这种方法: - 如果 Hash 值存在于服务器:文件可能存在也可能不存在(类似布隆过滤器的原理)。 - 如果 Hash 值不存在于服务器:文件一定不存在。  ### 2. 分片上传 上传时,将大文件分片,并逐片上传: - 将文件切分成多个小分片,每个分片单独作为一个网络请求上传。 - 使用异步线程从队列中每次取一个分片进行上传,避免并发过高导致网络拥堵。 - 每个分片有唯一标识,即 `hash值 + 分片编号`。 #### 断点续传 - 上传中断时,下次上传只需上传未完成的分片。 - 后端记录每个分片的上传状态,并返回未上传的分片标识给前端,前端根据这些标识继续上传未完成的分片。 ### 3. 合并请求 有两种方案来处理分片文件的合并: #### 自动合并 - 后端记录所有分片的上传状态。 - 当所有分片上传完毕后,自动合并文件。 #### 前端通知合并 - 前端在检测到所有分片上传完成后,调用接口通知后端进行合并。 ### 设计方案总结 #### 前端流程 1. **计算文件 Hash**: - 对文件进行抽样计算 Hash 值。 - 检查 Hash 值是否存在于服务器。 2. **分片上传**: - 将文件切分成多个分片。 - 逐片上传,每个分片带有 `hash值 + 分片编号`。 - 记录上传进度,支持断点续传。 3. **合并请求**: - 所有分片上传完成后,调用后端合并接口。 #### 后端流程 1. **检查文件存在**: - 根据 Hash 值判断文件是否已经存在。 2. **接收分片**: - 接收并存储分片文件。 - 记录分片上传状态。 3. **合并文件**: - 根据上传状态,合并所有分片文件。 ### 详细实现示例 #### 前端代码示例 ```javascript // 计算文件 Hash 值 function calculateFileHash(file) { return new Promise((resolve, reject) => { const chunkSize = 2 * 1024 * 1024; // 2MB const chunks = []; const reader = new FileReader(); let offset = 0; reader.onload = (e) => { chunks.push(e.target.result); offset += chunkSize; if (offset < file.size) { readNextChunk(); } else { const hash = md5(chunks.join('')); // 使用 MD5 计算 Hash resolve(hash); } }; function readNextChunk() { const blob = file.slice(offset, offset + chunkSize); reader.readAsArrayBuffer(blob); } readNextChunk(); }); } // 分片上传 function uploadFileInChunks(file, hash) { const chunkSize = 2 * 1024 * 1024; // 2MB const totalChunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); uploadChunk(chunk, hash, i); } } function uploadChunk(chunk, hash, index) { const formData = new FormData(); formData.append('file', chunk); formData.append('hash', hash); formData.append('index', index); fetch('/upload-chunk', { method: 'POST', body: formData }).then(response => { if (response.ok) { console.log(`Chunk ${index} uploaded successfully.`); } else { console.error(`Chunk ${index} failed to upload.`); } }); } // 合并请求 function mergeChunks(hash) { fetch('/merge-chunks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ hash }) }).then(response => { if (response.ok) { console.log('Chunks merged successfully.'); } else { console.error('Failed to merge chunks.'); } }); } ``` #### 后端代码示例 ```java import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.nio.file.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @RestController public class FileUploadController { private static final String UPLOAD_DIR = "./uploads/"; private Map<String, Set<Integer>> uploads = new ConcurrentHashMap<>(); @PostMapping("/upload-chunk") public Map<String, String> uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("hash") String hash, @RequestParam("index") int index) throws IOException { // 创建上传目录 File uploadDir = new File(UPLOAD_DIR); if (!uploadDir.exists()) { uploadDir.mkdirs(); } // 保存分片文件 File chunkFile = new File(UPLOAD_DIR + hash + "_" + index); try (FileOutputStream fos = new FileOutputStream(chunkFile)) { fos.write(file.getBytes()); } // 记录上传状态 uploads.computeIfAbsent(hash, k -> new HashSet<>()).add(index); Map<String, String> response = new HashMap<>(); response.put("status", "success"); return response; } @PostMapping("/merge-chunks") public Map<String, String> mergeChunks(@RequestBody Map<String, String> request) throws IOException { String hash = request.get("hash"); if (!uploads.containsKey(hash)) { Map<String, String> response = new HashMap<>(); response.put("status", "error"); response.put("message", "No chunks uploaded."); return response; } File mergedFile = new File(UPLOAD_DIR + hash); try (FileOutputStream fos = new FileOutputStream(mergedFile)) { for (int i : new TreeSet<>(uploads.get(hash))) { File chunkFile = new File(UPLOAD_DIR + hash + "_" + i); Files.copy(chunkFile.toPath(), fos); chunkFile.delete(); } } uploads.remove(hash); Map<String, String> response = new HashMap<>(); response.put("status", "success"); return response; } } ``` ### 结论 通过以上设计和实现,可以有效处理大文件上传过程中网络不稳定、断点续传等问题。分片上传和断点续传可以显著提高大文件上传的可靠性和用户体验。编辑于 2023-11-13 17:55 •IP属地 新疆举报评论收藏2
公众号进入网站体验更佳

