vue3+华为云obs(springboot)分段上传
springboot
pom依赖
<dependency>
<groupId>com.huaweicloud</groupId>
<artifactId>esdk-obs-java</artifactId>
<version>3.23.5</version>
</dependency>
代码块
package com.pig4cloud.pig.admin.controller;
import com.obs.services.ObsClient;
import com.obs.services.model.*;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@AllArgsConstructor
@RequestMapping("/upload")
public class SysUploadController {
private final ObsClient obsClient;
@GetMapping("/getUploadId/{objectKey}")
public String getUploadId(@PathVariable("objectKey")String objectKey) {
InitiateMultipartUploadRequest request = new InitiateMultipartUploadRequest("<桶名称>", objectKey);
ObjectMetadata metadata = new ObjectMetadata();
metadata.addUserMetadata("property", "property-value");
metadata.setContentType("text/plain");
request.setMetadata(metadata);
InitiateMultipartUploadResult result = obsClient.initiateMultipartUpload(request);
String uploadId = result.getUploadId();
return uploadId;
}
@PostMapping("/chunk")
public Map<String,String> splitFileUpload(
@RequestParam("objectKey")String objectKey,
@RequestParam("file") MultipartFile file,
@RequestParam("chunk") int chunk,
@RequestParam("uploadId") String uploadId) throws Exception {
File file1 = multipartFileToFile(file);
Map<String,String> map = uploadChunk(uploadId, file1, chunk, objectKey);
return map;
}
@PostMapping("/completeUpload")
public CompleteMultipartUploadResult completeUpload(
@RequestParam("objectKey")String objectKey,
@RequestParam("uploadId") String uploadId,
@RequestBody List<Map<String,String>> mapList
) {
List<PartEtag> partEtags = new ArrayList<>();
for(Map<String,String> map: mapList ){
PartEtag part1 = new PartEtag();
part1.setPartNumber(Integer.valueOf(map.get("partNumber")));
part1.seteTag(map.get("etag"));
partEtags.add(part1);
}
CompleteMultipartUploadRequest request = new CompleteMultipartUploadRequest(
"<桶名称>", objectKey, uploadId, partEtags);
CompleteMultipartUploadResult result = obsClient.completeMultipartUpload(request);
return result;
}
@GetMapping("/cancelUpload")
public void cancelUpload(
@RequestParam("objectKey")String objectKey,
@RequestParam("uploadId") String uploadId
){
AbortMultipartUploadRequest request = new AbortMultipartUploadRequest("<桶名称>", objectKey, uploadId);
obsClient.abortMultipartUpload(request);
}
public Map<String,String> uploadChunk(String uploadId, File file,int chunk, String objectKey){
Map<String,String> map = new HashMap<>();
UploadPartRequest request = new UploadPartRequest("<桶名称>", objectKey);
request.setUploadId(uploadId);
request.setPartNumber(chunk);
request.setFile(file);
request.setPartSize(5 * 1024 * 1024L);
UploadPartResult result = obsClient.uploadPart(request);
map.put("etag",result.getEtag());
map.put("partNumber",String.valueOf(result.getPartNumber()));
return map;
}
public static File multipartFileToFile(MultipartFile file) throws Exception {
File toFile = null;
if (file.equals("") || file.getSize() <= 0) {
file = null;
} else {
InputStream ins = null;
ins = file.getInputStream();
toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
ins.close();
}
return toFile;
}
private static void inputStreamToFile(InputStream ins, File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
obs初始化类
package com.pig4cloud.pig.admin.config;
import com.obs.services.ObsClient;
import com.pig4cloud.plugin.oss.OssProperties;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@AllArgsConstructor
public class ObsConfiguration {
private final OssProperties ossProperties;
@Bean
public ObsClient obsClient() {
return new ObsClient(ossProperties.getAccessKey(),
ossProperties.getSecretKey(),
ossProperties.getEndpoint());
}
}
vue3
<template>
<div class="upload-container">
<div class="fileUpload">
<div class="item-upload">
<span v-if="required" class="required">*</span>
<el-button type="primary" οnclick="document.getElementById('fpfileName').click()">文件选择</el-button>
<input type="file" id="fpfileName" class="file2" style="display: none" @change="selectFile" />
</div>
<div v-if="showFileName" class="item-info">
{{fileName}}
</div>
<div v-else class="item-info">
支持{{getAcceptType()}}文件格式
</div>
<div v-if="progress>0" class="item-cancel">
<el-button style="margin-top: 5px" size="small" type="danger" @click="cancelUploadInfo">X</el-button>
</div>
</div>
<div class="item-process" v-if="progress>0">
<el-progress :text-inside="true" :stroke-width="20" :percentage="progress"/>
</div>
<div v-if="requiredValid" class="item-valid">
请上传文件
</div>
</div>
</template>
<script>
import { getUploadId , uploadFileInfo, completeUpload, cancelUpload, getFileUrl} from "@/api/admin/client.js"
import { defineComponent, reactive, toRefs, onMounted } from "vue";
import { ElMessage } from 'element-plus'
export default defineComponent({
name: "file-upload",
props:{
//文件接受类型
accept:{
type: Array,
default: ['doc','docx','pdf']
},
//是否必填
required:{
type: Boolean,
default: false
},
//文件限制大小
fileSize:{
type: Number,
default: 500
}
},
setup(props) {
const data =reactive({
file: null,
fileName: "",
fileSize: 0,
progress: 0,
objectKey: "",
chunkSize: 5*1024 * 1024,
totalChunks: 0,
currentChunk: 1,
uploadId: "",
partArr: [],
showFileName: false,
bucketName: "",
objectKeyName: "",
required: props.required,
requiredValid: false
})
// 选择文件
const selectFile = (event)=>{
data.file = event.target.files[0];
event.target.value = ''
if(data?.file?.size){
const fileSize = Math.round(data?.file?.size/1024/1024*100)/100
if(fileSize > props.fileSize){
ElMessage.warning("文件大小超过上限"+props.fileSize+"MB")
return
}
data.fileSize = fileSize
console.log(data.fileSize)
const fileName = data.file.name
const index = fileName.lastIndexOf(".");
const fileType = fileName.substring(index+1, fileName.length)
if(!props.accept.includes(fileType)){
ElMessage.warning("文件类型不支持")
return
}
data.progress = 0;
data.fileName = data.file.name
data.showFileName = true
data.totalChunks = Math.ceil(data.file.size / data.chunkSize)
data.objectKey = guid()+"."+fileType
getUploadId(data.objectKey).then((res)=>{
data.uploadId = res.data
data.progress = 1;
uploadFile();
})
}
}
// 文件上传
const uploadFile = ()=>{
const index = data.currentChunk -1
const start = index * data.chunkSize;
const end = Math.min((index + 1) * data.chunkSize, data.file.size);
const formData = new FormData()
formData.append('file', data.file.slice(start, end))
formData.append('chunk', data.currentChunk)
formData.append('objectKey', data.objectKey)
formData.append('uploadId', data.uploadId)
// 调用后端接口上传切片数据
uploadFileInfo(formData).then((res) => {
data.partArr.push(res.data)
console.log(data.progress)
data.currentChunk++;
data.progress = Math.floor((data.currentChunk / data.totalChunks) * 100);
if (data.currentChunk <= data.totalChunks) {
uploadFile();
} else {
// 所有切片上传完成
data.progress = 99;
console.log(data.progress)
complete();
}
}).catch((error) => {
initParam()
console.error('切片上传失败:', error);
ElMessage.warning("文件上传异常或上传已取消")
});
}
// 上传完成并合并分段上传
const complete = () =>{
completeUpload(data.partArr,data.objectKey,data.uploadId).then((res) => {
console.log('文件上传完成');
console.log(res);
data.bucketName = res.data.bucketName
data.objectKeyName = res.data.objectKey
data.progress = 100;
initParam()
}).catch((error) => {
console.error('文件上传失败:', error);
ElMessage.error("文件上传失败")
});
}
// 取消上传
const cancelUploadInfo = () =>{
if(data.progress !== 100){
cancelUpload(data.objectKey,data.uploadId).then(()=>{
data.progress = 0;
data.showFileName = false
initParam()
})
}else{
data.progress = 0;
data.showFileName = false
initParam()
}
}
const getAcceptType = () =>{
let str = "";
if(props.accept){
for(let item of props.accept){
str += item +","
}
}
return str.substr(0,str.length-1)
}
// 构建uuid
const guid =() => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 初始化参数
const initParam = ()=>{
data.file = null;
data.objectKey = ""
data.totalChunks = 0
data.currentChunk = 1
data.uploadId = ""
data.partArr = []
data.requiredValid = false
}
// 必传校验
const validRequired = ()=>{
if(data.bucketName && data.objectKeyName){
data.requiredValid = false
return true
}else{
data.requiredValid = true
return false
}
}
// 返回上传结果 桶名称、文件名、文件大小
const getUploadInfo = () =>{
return {
bucketName: data.bucketName,
fileName: data.objectKeyName,
fileSize: data.fileSize
}
}
// 初始化
onMounted(()=>{
data.progress = 0;
})
return{
...toRefs(data),
selectFile,
uploadFile,
getAcceptType,
validRequired,
getUploadInfo,
cancelUploadInfo
}
}
})
</script>
<style scoped>
.upload-container{
.fileUpload {
display: flex;
flex-direction: row;
padding: 0px 8px;
.item-upload {
width: 100px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
.required{
color: red;
padding: 0px 5px;
}
}
.item-info {
flex: 1;
padding-top: 12px;
font-size: 13px;
}
.item-cancel{
width: 30px;
padding-right: 20px;
}
}
.item-process{
margin-top: 5px;
padding: 0px 23px;
box-sizing: border-box;
}
.item-valid{
color: red;
padding: 0px 10px;
font-size: 13px;
}
}
</style>
接口调用
export function getUploadId(objectKey) {
return request({
url: '/admin/upload/getUploadId/' + objectKey,
method: 'get'
})
}
export function uploadFileInfo( data) {
return request({
url: '/admin/upload/chunk',
method: 'post',
headers:{'Content-Type': 'multipart/form-data'},
data: data
})
}
export function completeUpload( data,objectKey,uploadId) {
return request({
url: '/admin/upload/completeUpload?objectKey='+objectKey+'&uploadId='+uploadId,
method: 'post',
data: data
})
}
export function cancelUpload(objectKey,uploadId) {
return request({
url: '/admin/upload/cancelUpload?objectKey='+objectKey+'&uploadId='+uploadId,
method: 'get'
})
}