-
后端上传分片方法和合并分片方法
/** * 分片上传文件 * @param file * @param chunkIndex -分片序号 * @param totalChunks - 总分片数 * @param fileName * @return */ @PostMapping("/chunk") public R uploadChunk(@RequestParam("file") MultipartFile file, @RequestParam("chunkIndex") int chunkIndex, @RequestParam("totalChunks") int totalChunks, @RequestParam(value = "fileName") String fileName, @RequestParam("objectName") String objectName, @RequestParam("dir") String dir) { try { minioPartUploadService.uploadChunk(file,chunkIndex,totalChunks,fileName,objectName,dir); } catch (Exception e) { e.printStackTrace(); } return R.ok(); } /** * 合并分片上传文件 * @param fileName * @param totalChunks 总分片数 * @return */ @PostMapping("/merge") public R mergeChunks(@RequestParam("fileName") String fileName,@RequestParam("objectName") String objectName,@RequestParam("dir") String dir,@RequestParam("totalChunks") int totalChunks) { return minioPartUploadService.mergeChunks(fileName,objectName,dir,totalChunks); } package com.shpl.scp.admin.service.impl; import com.shpl.scp.admin.config.MinioConfiguration; import com.shpl.scp.admin.service.MinioPartUploadService; import com.shpl.scp.admin.service.SysFileService; import com.shpl.scp.common.core.util.R; import io.minio.*; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.poi.util.StringUtil; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @Slf4j @Service @RequiredArgsConstructor public class MinioPartUploadServiceImpl implements MinioPartUploadService { private final MinioClient minioClient; private final MinioConfiguration configuration; private final SysFileService sysFileService; @Override public void uploadChunk(MultipartFile file,int chunkIndex,int totalChunks,String fileName,String objectName,String dir) { try { if (StringUtil.isNotBlank(dir)) { // 确保 dir 以 '/' 结尾,避免路径拼接错误 dir = !dir.endsWith("/") ? dir + "/" : dir; objectName = dir + objectName; } String chunkFileName = objectName + "_" + chunkIndex; minioClient.putObject(PutObjectArgs.builder() .bucket(configuration.getBucketName()) .object(chunkFileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); } catch (Exception e) { log.error("上传分片异常 Error uploading chunk: " + e.getMessage()); e.printStackTrace(); } } @Override public R mergeChunks(String fileName, String objectName, String dir, int totalChunks) { Map<String, String> resultMap = new HashMap<>(4); try { resultMap.put("bucketName", configuration.getBucketName()); resultMap.put("fileName", objectName); resultMap.put("url", String.format("/admin/sys-file/oss/file?fileName=%s", objectName)); //保存上传文件记录 sysFileService.savefileLog(fileName,objectName,dir,null); if (StringUtil.isNotBlank(dir)) { // 确保 dir 以 '/' 结尾,避免路径拼接错误 dir = !dir.endsWith("/") ? dir + "/" : dir; objectName = dir + objectName; } String mergedFileName = objectName;//objectName + "_merged"; // 收集所有分片作为合并源 List<ComposeSource> sourceObjects = new ArrayList<>(); for (int i = 0; i < totalChunks; i++) { String chunkFileName = objectName + "_" + i; // 添加分片到源列表 sourceObjects.add(ComposeSource.builder() .bucket(configuration.getBucketName()) .object(chunkFileName) .build()); } // 使用 composeObject 在服务端合并所有分片 minioClient.composeObject( ComposeObjectArgs.builder() .bucket(configuration.getBucketName()) .object(mergedFileName) .sources(sourceObjects) .build() ); // 删除分片文件 for (int i = 0; i < totalChunks; i++) { String chunkFileName = objectName + "_" + i; minioClient.removeObject(RemoveObjectArgs.builder() .bucket(configuration.getBucketName()) .object(chunkFileName) .build()); } } catch (Exception e) { log.error("合并分片异常 Error mergeChunks : " + e.getMessage()); } return R.ok(resultMap); } } -
vue3+elementPlus前端分片,每片5M并发上传
<el-col :span="12" class="mb20"> <el-form-item label="附件" prop="attribute2"> <el-upload class="upload-demo" multiple :limit="100" :http-request="handleCustomUpload" :on-progress="handleCustomUploadProgress" :on-success="handleCustomUploadSuccess" :on-error="handleCustomUploadError"> <el-button type="primary">点击上传</el-button> <template #tip> <div class="el-upload__tip"> 支持大文件分片上传,每个分片 5MB,自动重试失败分片 </div> </template> </el-upload> </el-form-item> </el-col> // 上传进度处理 const handleCustomUploadProgress = (event: any, file: any, fileList: any[]) => { console.log(`上传进度:${file.name} - ${event.percent}%`); }; // 上传成功处理 const handleCustomUploadSuccess = (response: any, file: any, fileList: any[]) => { console.log(`${file.name} 上传成功`, response); // 可以在这里保存文件 URL 到表单 // form.attribute2 = response.data?.url; }; // 上传失败处理 const handleCustomUploadError = (error: any, file: any, fileList: any[]) => { console.error(`${file.name} 上传失败`, error); }; /** * 分片上传文件 * @param file - 要上传的文件对象 * @param onProgress - 进度回调函数 (可选) * @returns 上传结果 */ async function uploadPartFile(file: File, onProgress?: (percent: number) => Promise<void> | void) { const chunkSize = 5 * 1024 * 1024; // 5MB per chunk const totalChunks = Math.ceil(file.size / chunkSize); const uploadedChunks = new Set<number>(); const maxRetries = 3; const concurrentLimit = 3; // 最多同时上传 3 个分片 // 上传单个分片 const uploadChunk = async (chunkIndex: number, retryCount = 0): Promise<void> => { try { console.log(`上传分片 ${chunkIndex + 1}/${totalChunks}`); const start = chunkIndex * chunkSize; const end = Math.min((chunkIndex + 1) * chunkSize, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('file', chunk); formData.append('chunkIndex', chunkIndex.toString()); formData.append('totalChunks', totalChunks.toString()); formData.append('fileName', file.name); // 使用项目的 request 工具 await request({ url: '/admin/sys-file/chunk', method: 'post', data: formData, headers: { 'Content-Type': 'multipart/form-data', }, }); uploadedChunks.add(chunkIndex); // 更新进度 if (onProgress) { const percent = Math.round((uploadedChunks.size / totalChunks) * 100); await onProgress(percent); } } catch (error) { // 失败重试 if (retryCount < maxRetries) { console.warn(`分片 ${chunkIndex} 上传失败,第 ${retryCount + 1} 次重试...`); await uploadChunk(chunkIndex, retryCount + 1); } else { throw new Error(`分片 ${chunkIndex} 上传失败,已达到最大重试次数`); } } }; // 并发控制上传 const uploadWithConcurrency = async () => { const queue: number[] = []; for (let i = 0; i < totalChunks; i++) { queue.push(i); } const workers: Promise<void>[] = []; while (queue.length > 0 || workers.length > 0) { // 填充 worker 到并发限制 while (workers.length < concurrentLimit && queue.length > 0) { const chunkIndex = queue.shift()!; const worker = uploadChunk(chunkIndex).finally(() => { const index = workers.indexOf(worker); if (index > -1) workers.splice(index, 1); }); workers.push(worker); } // 等待至少一个 worker 完成 if (workers.length > 0) { await Promise.race(workers); } } await Promise.all(workers); }; try { // 开始上传所有分片 await uploadWithConcurrency(); console.log(`合并分片`+ file.name); // 合并分片 await request({ url: '/admin/sys-file/merge', method: 'post', data: { fileName: file.name, totalChunks: totalChunks, }, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); console.log(`文件 ${file.name} 上传成功,共 ${totalChunks} 个分片`); return { success: true, fileName: file.name }; } catch (error: any) { console.error('文件上传失败:', error); throw new Error(`文件上传失败:${error.message}`); } } /** * el-upload 组件的自定义上传方法 * @param options - 上传选项 */ const handleCustomUpload = async (options: any) => { const { file, onSuccess, onError, onProgress } = options; try { await uploadPartFile(file, (percent) => { if (onProgress) { onProgress({ percent }); } }); if (onSuccess) { onSuccess({ code: 0, message: '上传成功' }); } useMessage().success('文件上传成功'); } catch (error: any) { console.error('上传失败:', error); if (onError) { onError(error); } useMessage().error(`文件上传失败:${error.message}`); } };或者如下js方法:
async function uploadFile(file) { const chunkSize = 5 * 1024 * 1024; // 5MB const totalChunks = Math.ceil(file.size / chunkSize); for (let i = 0; i < totalChunks; i++) { const chunk = file.slice(i * chunkSize, (i + 1) * chunkSize); const formData = new FormData(); formData.append("file", chunk); formData.append("chunkIndex", i); formData.append("totalChunks", totalChunks); formData.append("fileName", file.name); await fetch("/upload/chunk", { method: "POST", body: formData }); } await fetch("/upload/merge", { method: "POST", body: JSON.stringify({ fileName: file.name, totalChunks: totalChunks }), headers: { "Content-Type": "application/json" } }); }
springboot3+vue3+elementPlus+minio8.2 大文件分片上传
xiaogg36782026-03-13 12:07
相关推荐
phltxy2 小时前
前缀和算法:从一维到二维,解锁高效区间求和码上淘金2 小时前
Prometheus 瘦身指南:小白也能看懂的指标过滤与标签优化一轮弯弯的明月2 小时前
竞赛刷题-建造最大岛屿-Java版weixin199701080162 小时前
开山网商品详情页前端性能优化实战Memory_荒年2 小时前
AQS:Java并发包里的“包租公”,管理着你的锁和通行证!小钻风33662 小时前
Java 8 流式编程肯戳加勾2 小时前
JAVA最常见的装箱/拆箱坑Memory_荒年2 小时前
ReentrantLock:AQS家的“锁二代”,但比 synchronized 更会“来事儿”巫山老妖2 小时前
OpenClaw 心跳机制实战:让 AI Agent 24 小时不停自主运行