赞
踩
使用了开源组件:x-file-storage gitee地址 组件官网:官网
@Autowired private FileStorageService fileStorageService; /** * 初始化 分片信息 * @param fileUploadInfo * @return */ @GetMapping(value = "/uploader/chunk") public FileInfo init(FileUploadInfo fileUploadInfo) { return fileStorageService.initiateMultipartUpload() .setSize(fileUploadInfo.getTotalSize()) .setOriginalFilename(fileUploadInfo.getFilename()) .init(); } /** * 上传 分片文件 * @param fileUploadInfo * @return */ @PostMapping(value = "/uploader/chunk") public void bigFile(FileUploadInfo fileUploadInfo) { final FileInfo fileInfo = fileStorageService.getFileInfoByUrl(fileUploadInfo.getUrl()); final MultipartFileWrapper fileWrapper = new MultipartFileWrapper( fileUploadInfo.getUpfile(), fileUploadInfo.getUpfile().getOriginalFilename(), fileUploadInfo.getUpfile().getContentType(), fileUploadInfo.getUpfile().getSize()); fileStorageService.uploadPart(fileInfo, fileUploadInfo.getChunkNumber(), fileWrapper, fileUploadInfo.getCurrentChunkSize().longValue()).upload(); } /** * 合并 分片文件 * @param url * @return */ @PostMapping(value = "/uploader/merge") public FileInfo merge(@RequestParam String url) { final FileInfo fileInfo = fileStorageService.getFileInfoByUrl(url); return fileStorageService.completeMultipartUpload(fileInfo).complete(); }
@Data public class FileUploadInfo { /** * 操作 文件 的key */ private String url; private Integer chunkNumber; private Integer chunkSize; private Integer currentChunkSize; private Long totalSize; private String identifier; private String filename; private String relativePath; private Integer totalChunks; private MultipartFile upfile; }
docker pull minio/minio
mkdir -p /opt/minio/{data,config}
docker run \
-p 9000:9000 \
-p 9090:9090 \
--name minio \
-e "MINIO_ROOT_USER=minio" \
-e "MINIO_ROOT_PASSWORD=minio123456" \
-v /opt/minio/data:/data \
-v /opt/minio/config:/root/.minio \
minio/minio server /data --console-address ":9090" -address ":9000"
server: port: 8000 spring: datasource: url: jdbc:h2:file:./data/test_file/file;AUTO_SERVER=TRUE;MODE=MYSQL;DB_CLOSE_DELAY=-1;AUTO_RECONNECT=TRUE username: root password: 123456 driver-class-name: org.h2.Driver servlet: multipart: max-file-size: 100MB max-request-size: 201MB dromara: x-file-storage: default-platform: minio minio: - platform: minio enable-storage: true access-key: zygoJJAKsIZHyXkdKfa4 secret-key: RBd3Coi6LJnJPEhv6IEUEEnhm0Mums4VMi1bS6SK end-point: http://192.168.64.129:9000 bucket-name: test domain: base-path:
<template> <div> <!-- 上传器 --> <uploader ref="uploaderRef" :options="options" :autoStart="false" :file-status-text="fileStatusText" class="uploader-ui" @file-added="onFileAdded" @file-success="onFileSuccess" @file-progress="onFileProgress" @file-error="onFileError" > <uploader-unsupport></uploader-unsupport> <uploader-drop> <div> <uploader-btn id="global-uploader-btn" ref="uploadBtn" :attrs="attrs"> 选择文件 <el-icon><Upload /></el-icon> </uploader-btn> </div> </uploader-drop> <uploader-list></uploader-list> </uploader> </div> </template> <script setup> import { ACCEPT_CONFIG } from '@/config/accept.ts'; import { reactive, ref } from 'vue'; import SparkMD5 from 'spark-md5'; import { mergeFile } from '@/api/fileUpload/index'; import { ElMessage } from 'element-plus'; const options = reactive({ //目标上传 URL,默认POST, import.meta.env.VITE_API_URL = api // target ==》http://localhost:6666/api/uploader/chunk target: import.meta.env.VITE_API_URL + '/uploader/chunk', query: {}, headers: { // 需要携带token信息,当然看各项目情况具体定义 token: "your_token", }, //分块大小(单位:字节) chunkSize: '5242880', url: '', //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file fileParameterName: 'upfile', //失败后最多自动重试上传次数 maxChunkRetries: 3, //是否开启服务器分片校验,对应GET类型同名的target URL testChunks: true, // 服务器分片校验函数 checkChunkUploadedByResponse: function (chunk, response_msg) { let objMessage = JSON.parse(response_msg); if (!this.url) { options.url = objMessage?.url; } if (objMessage?.attr?.skipUpload) { return true; } return (objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0; }, processParams: function(params, file) { if (!params.url) { params.url = options.url } return params } }); const attrs = reactive({ accept: ACCEPT_CONFIG.getAll(), }); const fileStatusText = reactive({ success: '上传成功', error: '上传失败', uploading: '上传中', paused: '暂停', waiting: '等待上传', }); onMounted(() => { }); function onFileAdded(file) { computeMD5(file); } function onFileSuccess(rootFile, file, response, chunk) { //refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等 file.refProjectId = ''; mergeFile(options.url) .then((responseData) => { if (responseData.data.code === 415) { console.log('合并操作未成功,结果码:' + responseData.data.code); } ElMessage.success(responseData.data); options.url = ''; }) .catch(function (error) { console.log('合并后捕获的未知异常:' + error); }) } function onFileError(rootFile, file, response, chunk) { console.log('上传完成后异常信息:' + response); } function onFileProgress(rootFile, file, chunk) { // 文件进度的回调 // console.log('on-file-progress', rootFile, file, chunk) } /** * 计算md5,实现断点续传及秒传 * @param file */ function computeMD5(file) { file.pause(); //单个文件的大小限制2G let fileSizeLimit = 2 * 1024 * 1024 * 1024; console.log('文件大小:' + file.size); console.log('限制大小:' + fileSizeLimit); if (file.size > fileSizeLimit) { file.cancel(); } let fileReader = new FileReader(); let time = new Date().getTime(); let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; let currentChunk = 0; const chunkSize = 10 * 1024 * 1000; let chunks = Math.ceil(file.size / chunkSize); let spark = new SparkMD5.ArrayBuffer(); //由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式 let chunkNumberMD5 = 1; loadNext(); fileReader.onload = (e) => { spark.append(e.target.result); if (currentChunk < chunkNumberMD5) { loadNext(); } else { let md5 = spark.end(); file.uniqueIdentifier = md5; file.resume(); console.log( `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${ file.size } 用时:${new Date().getTime() - time} ms` ); } }; fileReader.onerror = function () { error(`文件${file.name}读取出错,请检查该文件`); file.cancel(); }; function loadNext() { let start = currentChunk * chunkSize; let end = start + chunkSize >= file.size ? file.size : start + chunkSize; fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end)); currentChunk++; console.log('计算第' + currentChunk + '块'); } } const uploaderRef = ref(); function close() { uploaderRef.value.cancel(); } function error(msg) { console.log(msg, 'msg'); } </script> <style scoped> .uploader-ui { padding: 15px; margin: 40px auto 0; font-size: 12px; font-family: Microsoft YaHei; box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); } .uploader-ui .uploader-btn { margin-right: 4px; font-size: 12px; border-radius: 3px; color: #fff; background-color: #409eff; border-color: #409eff; display: inline-block; line-height: 1; white-space: nowrap; } .uploader-ui .uploader-list { max-height: 440px; overflow: auto; overflow-x: hidden; overflow-y: auto; } </style>
与上面的参考代码差不多,不过在兼容后端有一些调整。以下为差异点
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。