此处后端使用的是前期封装的自定义starter,具体链接可参考:minio对象存储spring boot starter封装组件
这里主要针对前期封装的组件,做一个简单的应用,前端直传可查看之前的文章
秒传的逻辑比较简单,在前传上传之前,先获取到对应文件的md5,传给后端,后端校验md5是否已经存在,存在则直接提示上传成功,否则,发起文件上传请求
获取文件MD5,前端可以使用spark-md5
或者crypto-js
<el-upload class="upload-demo" drag action="#" :http-request="sparkUploadHandle" :show-file-list="false">
<el-icon class="el-icon--upload"><UploadFilled />el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传em>div>
el-upload>
对应的处理逻辑
<script setup lang="ts">
import { ElMessage } from 'element-plus'
import SparkMD5 from 'spark-md5'
import { checkMd5 } from '@/api/file'
import { UploadFilled } from '@element-plus/icons-vue'
const sparkUploadHandle = (param: any) => {
const file = param.file
const fileReader = new FileReader()
const Spark = new SparkMD5.ArrayBuffer()
fileReader.readAsArrayBuffer(file)
fileReader.onload = function (e) {
Spark.append(e.target.result)
const md5 = Spark.end()
ElMessage.success('文件MD5:' + md5)
//上传逻辑处理
const data = {
md5: md5,
fileName: file.name
}
checkMd5(data).then(resp => {
if (resp.code == 201) {
ElMessage.success('秒传成功')
} else {
ElMessage.info('后台无数据,正在上传中....')
//请求上传接口
}
})
}
}
</script>
checkMd5
就是请求后端接口,查看是否存在
这里使用的是前端直传方式的分片上传,处理逻辑:前端根据指定的大小对文件进行分片,分片完成后,根据文件名和分片数量去请求后端,获得对应的分片上传地址集合,再根据返回的地址集合,进行前端直传,传完后,调用后端接口,合并分片
后端拿到对应的文件名称,分片大小和文件类型,然后返回给前端对应的put直传地址集合,每个直传地址默认10分钟的有效期
@Autowired
private MinioService minioService;
@GetMapping("/part-url")
public RestResult<MultiPartUploadInfo> partUrl(@RequestParam String fileName,
@RequestParam int partSize, @RequestParam String contentType) throws MinioException {
MultiPartUploadInfo uploadInfo = minioService.initMultiPartUploadId("bucketName", fileName, partSize, contentType);
return RestResult.ok(uploadInfo);
}
@GetMapping("/merge-part")
public RestResult<String> mergePart(@RequestParam String fileName, @RequestParam String uploadId) throws MinioException {
String merge = minioService.mergeMultiPartUpload("bucketName", fileName, uploadId);
return RestResult.ok(merge);
}
<el-upload class="upload-demo" drag action="#" :http-request="partUploadHandle" :show-file-list="false">
<el-icon class="el-icon--upload"><UploadFilled />el-icon>
<div class="el-upload__text">将文件拖到此处,或<em>点击上传em>div>
el-upload>
import { reactive } from 'vue'
import service from '@/utils/request'
import { getPartUrl, mergePart } from '@/api/file'
import { UploadFilled } from '@element-plus/icons-vue'
const state = reactive({
uploadId: ''
})
//分片大小
const chunkSize = 50 * 1024 * 1024
const partUploadHandle = (param: any) => {
let file = param.file
// 正在创建分片
let fileChunks = createFileChunk(file)
let data = {
fileName: file.name,
partSize: fileChunks.length,
contentType: file.type
}
//获得上传的url
getPartUrl(data).then(resp => {
state.uploadId = resp.data.uploadId
let uploadUrls = resp.data.uploadUrls
if (fileChunks.length !== uploadUrls.length) {
ElMessage.error('文件分片上传地址获取错误')
return
}
let chunkList = []
fileChunks.map((chunkItem, index) => {
chunkList.push({
chunkNumber: index + 1,
chunk: chunkItem,
uploadUrl: uploadUrls[index],
progress: 0,
status: '—'
})
})
//上传分片
uploadChunkBase(chunkList, file.type).then(resp => {
console.log('分片上传完成')
let par = {
fileName: file.name,
uploadId: state.uploadId
}
//请求后端合并分片
mergePart(par).then(resp => {
ElMessage.info('上传成功,访问地址:' + resp.data)
})
})
})
}
/**
* 文件分片
*/
const createFileChunk = (file, size = chunkSize) => {
const fileChunkList = []
let count = 0
while (count < file.size) {
fileChunkList.push({
file: file.slice(count, count + size)
})
count += size
}
return fileChunkList
}
//分片上传
const uploadChunkBase = (chunkList, contentType = 'application/octet-stream') => {
let successCount = 0
let totalChunks = chunkList.length
return new Promise<void>((resolve, reject) => {
const handler = () => {
if (chunkList.length) {
const chunkItem = chunkList.shift()
// 直接上传二进制,不需要构造 FormData,否则上传后文件损坏
service
.put(chunkItem.uploadUrl, chunkItem.chunk.file, {
headers: {
'Content-Type': contentType
}
})
.then(response => {
if (response.status === 200) {
console.log('分片:' + chunkItem.chunkNumber + ' 上传成功')
successCount++
// 继续上传下一个分片
handler()
} else {
// 注意:这里没有针对失败做处理,请根据自己需求修改
console.log('上传失败:' + response.status + ',' + response.statusText)
}
})
.catch(error => {
// 更新状态
console.log('分片:' + chunkItem.chunkNumber + ' 上传失败,' + error)
// 重新添加到队列中
chunkList.push(chunkItem)
handler()
})
}
if (successCount >= totalChunks) {
resolve()
}
}
// 支持10个并发
for (let i = 0; i < 10; i++) {
handler()
}
})
}
utils/request
是对axios的封装,比如超时,返回体错误码判断等等
对应的 api/file.ts
import service from '@/utils/request'
/** MD5校验 */
export const checkMd5 = (params?: object) => {
return service.get('/oss/check', { params: params })
}
/** 获取分片上传地址 */
export const getPartUrl = (params?: object) => {
return service.get('/oss/part-url', { params: params })
}
/** 分片合并 */
export const mergePart = (params?: object) => {
return service.get('/oss/merge-part', { params: params, timeout: 10000 })
}
至此,分片上传即可使用
这个其实就是在分片上传的基础做一个改进,将上传完成的分片反馈给后端做记录,再次续传时,只传对应的未上传分片即可,上传完成,再请求后端合并。