@RestController @RequestMapping("/file") public class FileController { @Autowired FileService fileService; @PostMapping("/upload1") public ResponseEntity<String> secondUpload(@RequestParam(value = "file",required = false) MultipartFile file,@RequestParam(required = false,value = "md5") String md5) { try { // 检查数据库中是否已存在该文件 if (fileService.existsByMd5(md5)) { return ResponseEntity.ok("文件已存在"); } // 堆代码 duidaima.com // 保存文件到服务器 file.transferTo(new File("/path/to/save/" + file.getOriginalFilename())); // 保存文件信息到数据库 fileService.save(new FileInfo(file.getOriginalFilename(), DigestUtils.md5DigestAsHex(file.getInputStream()))); return ResponseEntity.ok("上传成功"); } catch (Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("上传失败"); } } }前端调用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>秒传</title> <script src="spark-md5.js"></script> </head> <body> <input type="file" id="fileInput" /> <button onclick="startUpload()">开始上传</button> <hr> <script> async function startUpload() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert("请选择文件"); return; } const md5 = await calculateMd5(file); const formData = new FormData(); formData.append('md5', md5); const response = await fetch('/file/upload1', { method: 'POST', body: formData }); const result = await response.text(); if (response.ok) { if (result != "文件已存在") { // 开始上传文件 } } else { console.error("上传失败: " + result); } } function calculateMd5(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => { const spark = new SparkMD5.ArrayBuffer(); spark.append(reader.result); resolve(spark.end()); }; reader.onerror = () => reject(reader.error); reader.readAsArrayBuffer(file); }); } </script> </body> </html>前端分为两个步骤:
2.如果文件已经存在,则不需要继续上传文件;如果文件不存在,则开始上传文件,上传文件和 MD5 校验请求类似
private static final String UPLOAD_DIR = System.getProperty("user.home") + "/uploads/"; /** * 上传文件到指定位置 * * @param file 上传的文件 * @param start 文件开始上传的位置 * @return ResponseEntity<String> 上传结果 */ @PostMapping("/upload2") public ResponseEntity<String> resumeUpload(@RequestParam("file") MultipartFile file, @RequestParam("start") long start,@RequestParam("fileName") String fileName) { try { File directory = new File(UPLOAD_DIR); if (!directory.exists()) { directory.mkdirs(); } File targetFile = new File(UPLOAD_DIR + fileName); RandomAccessFile randomAccessFile = new RandomAccessFile(targetFile, "rw"); FileChannel channel = randomAccessFile.getChannel(); channel.position(start); channel.transferFrom(file.getResource().readableChannel(), start, file.getSize()); channel.close(); randomAccessFile.close(); return ResponseEntity.ok("上传成功"); } catch (Exception e) { System.out.println("上传失败: "+e.getMessage()); return ResponseEntity.status(500).body("上传失败"); } }后端每次处理的时候,需要先设置文件的起始位置。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>分片示例</title> </head> <body> <input type="file" id="fileInput" /> <button onclick="startUpload()">开始上传</button> <script> async function startUpload() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert("请选择文件"); return; } const filename = file.name; let start = 0; uploadFile(file, start); } async function uploadFile(file, start) { const chunkSize = 1024 * 1024; // 每个分片1MB const total = Math.ceil(file.size / chunkSize); for (let i = 0; i < total; i++) { const chunkStart = start + i * chunkSize; const chunkEnd = Math.min(chunkStart + chunkSize, file.size); const chunk = file.slice(chunkStart, chunkEnd); const formData = new FormData(); formData.append('file', chunk); formData.append('start', chunkStart); formData.append('fileName', file.name); const response = await fetch('/file/upload2', { method: 'POST', body: formData }); const result = await response.text(); if (response.ok) { console.log(`分片 ${i + 1}/${total} 上传成功`); } else { console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`); break; } } } </script> </body> </html>五 断点续传实战
2.前端根据已经存在的大小,继续上传文件即可。
@GetMapping("/check") public ResponseEntity<Long> checkFile(@RequestParam("filename") String filename) { File file = new File(UPLOAD_DIR + filename); if (file.exists()) { return ResponseEntity.ok(file.length()); } else { return ResponseEntity.ok(0L); } }.如果文件存在,则返回已经存在的文件大小。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>断点续传示例</title> </head> <body> <input type="file" id="fileInput"/> <button onclick="startUpload()">开始上传</button> <script> async function startUpload() { const fileInput = document.getElementById('fileInput'); const file = fileInput.files[0]; if (!file) { alert("请选择文件"); return; } const filename = file.name; let start = await checkFile(filename); uploadFile(file, start); } async function checkFile(filename) { const response = await fetch(`/file/check?filename=${filename}`); const start = await response.json(); return start; } async function uploadFile(file, start) { const chunkSize = 1024 * 1024; // 每个分片1MB const total = Math.ceil((file.size - start) / chunkSize); for (let i = 0; i < total; i++) { const chunkStart = start + i * chunkSize; const chunkEnd = Math.min(chunkStart + chunkSize, file.size); const chunk = file.slice(chunkStart, chunkEnd); const formData = new FormData(); formData.append('file', chunk); formData.append('start', chunkStart); formData.append('fileName', file.name); const response = await fetch('/file/upload2', { method: 'POST', body: formData }); const result = await response.text(); if (response.ok) { console.log(`分片 ${i + 1}/${total} 上传成功`); } else { console.error(`分片 ${i + 1}/${total} 上传失败: ${result}`); break; } } } </script> </body> </html>这个案例实际上是一个断点续传+分片上传的案例,相关知识点并不难,小伙伴们可以自行体会下。