Uniapp + Vue3+Minio 实现前端直传文件至minio,大文件可分片,支持断点续传

241 阅读2分钟

心路历程

这一段话,我是必需要写的,因为这个方案是经历了三次大修改,才找到这种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通知服务器合并文件。

逻辑处理流程图

APP传文件至minio流程图.jpg

主要代码

服务器获取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>