心路历程
这一段话,我是必需要写的,因为这个方案是经历了三次大修改,才找到这种APP直传minio的方案。不知道大家有没有和我一样的经历。 最开始的两个方案,都因为minio需要用put才能上传文件,但uniapp没有封装put而选用了其他方式。当然我也尝试使用了xml请求和axios来封装put,但都不太理想,这里上传的文件的格式必须要是一个文件,uniAPP这里需要转换,很麻烦。
主要技术
uniapp、Vue3、Minio、SpringBoot。 分片的插件地址 Forke-FileSpliter ext.dcloud.net.cn/plugin?id=3…
方案一
APP把分片文件,传给服务器,服务器接收文件再上传至minio上,反馈信息给APP。 缺点:
- 因网速原因,上传很慢
- 服务器判断分片是否全部上传,因为线程和数据库锁的问题,经常导致合并文件失败
- 分片文件缺失
- APP内显示的上传进度条不真实
方案二
APP传分片至服务器,服务接收文件,利用WebSocket发送信息个APP,APP展示进度和判断是否上传完成来合并文件。 缺点:
- WebSocket在不同的真机下会接收不到消息,导致无法显示进度和合并文件
方案三
服务器利用getPresignedPostFormData 获取使用post上传的参数和url,由APP通过post上传文件至minio,APP通知服务器合并文件。
逻辑处理流程图
主要代码
服务器获取postUrl
详细参考这个方法,官网上有api
minioClient.getPresignedPostFormData // 获取地址
minioClient.listObjects // 查询文件,可判断分片是否上传完成
获取分片信息
<template>
<view>
<view>
<text>{{video.filePath}}</text>
<text>{{video.size}}</text>
</view>
<button @click="getVideo()">选择视频</button>
<button type="primary" style="width: 100%;" @click="complete()">合并</button>
</view>
</template>
<script>
import Ajax from '../../util/Ajax';
const FileSpliter = uni.requireNativePlugin('Forke-FileSpliter');
const partChunkSize = 1024 * 1024 * 5;
const chunkFileSavePath = '_doc/chunk/'; // 分片存储的位置
export default {
data() {
return {
video: { filePath: '', size: '' },
}
},
methods: {
getVideo() {
uni.chooseVideo({
compressed: false,
success: (res) => {
const filePath = res.tempFilePath;
this.video = {
...res,
filePath
}
let videoChunk = Math.ceil(res.size / partChunkSize);
Ajax.post('app/mino/creatPartPost', { number: videoChunk }, { dataType: 'form' }, (data) => {
let netInfo = data.data;
let chunkMap = new Map();
chunkMap.set('fileName', filePath.split('/').pop())
chunkMap.set('baseKey', data.data.key);
netInfo.forms.forEach((item) => {
chunkMap.set(item.number, {
'x-amz-date': item.xamzdate,
'x-amz-signature': item.xamzsignature,
'x-amz-algorithm': item.xamzalgorithm,
'x-amz-credential': item.xamzcredential,
'policy': item.policy,
'key': item.key,
})
});
let doneNum = 0;
plus.io.resolveLocalFileSystemURL(filePath, (entry) => {
entry.file((file) => {
let url = plus.io.convertLocalFileSystemURL(chunkFileSavePath);
FileSpliter.splitFile({
filePath: file.fullPath, //选择文件的完整路径,例如"/storage/0/..."
savePath: url, //保存文件的完整路径,需要该路径存在, 例如"/storage/0/..."
fileName: file.name, //"文件名"
chunkSize: partChunkSize //每一片的大小, 例如 1024 * 1024 * 10 代表10MB
}, (ret) => {
//成功的回调
if (ret) { // { chunk: 分片的序号, name: 分片所属文件名原片文件名, path:分片的绝对路径 }
if (ret.code == 'process') {
uni.uploadFile({
url: netInfo.url,
filePath: ret.path,
name: 'file',
formData: chunkMap.get(ret.chunk),
complete: (res) => {
if (res.statusCode == 204) {
doneNum += 1;
if (doneNum === videoChunk) {
this.completeFile(chunkMap.get('baseKey'), videoChunk, chunkMap.get('fileName'));
}
}
}
});
}
}
}, (ret) => {
//失败的回调
});
})
}, function ( e ) {
} )
})
}
})
},
completeFile(baseKey, number, targetName) {
Ajax.post('app/mino/completePartPost', { baseKey, number, targetObjectName: targetName }, { dataType: 'form' }, (data) => {
if (data.flag === 0) {
this.deleteFiles(targetName);
}
});
},
// 删除文件
deleteFiles(fileName){
FileSpliter.clearFilePath({
savePath: plus.io.convertLocalFileSystemURL(chunkFileSavePath),
fileName: fileName
}, (ret) => {
if (ret) {
if (ret.code == 'complete') {
console.log('切片目录已清空');
}
}
}, (ret) => {
});
}
}
}
</script>
<style>
</style>