vue3+华为云obs(springboot)分段上传

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){
        // Endpoint以北京四为例,其他地区请按实际情况填写。
        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;
    }

    /**
     * MultipartFile 转 File
     *
     * @param file
     * @throws Exception
     */
    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 {

    // 请在yml里配置或使用 @Value 单独获取
    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>
接口调用
// 获取uploadId
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'
  })
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值