Vue大文件上传 vuesimpleloader分片上传到AWS S3

前言

前端使用vue2.0,上传组件为vue-simple-loader分片上传文件。

后台使用java8接收,接收文件后,保存在项目路径下,分片上传到AWS S3存储桶。

流程:大文件通过vue-simple-loader分片上传到java后台,保存到本地项目下。再将本地项目下的文件分片上传到s3,上传成功后,删除本地文件。

现存待研究问题:

1. vue-simple-loader上传一个分片,s3接收一个分片的形式,但未实现(暂未找到s3接收此种形式的方法)。

2.通过js直连s3进行上传,js版本2方式:https://www.cnblogs.com/aiyowei/p/15769695.html。js版本3方式暂未实现

参考的大佬笔记:

vue-simple-loader github链接:https://github.com/simple-uploader/vue-uploader/blob/master/README_zh-CN.md

vue-simple-upload options属性 github链接:https://github.com/simple-uploader/Uploader#events

vue-simple-uploader笔记:https://www.cnblogs.com/xiahj/p/vue-simple-uploader.html

后台分片上传笔记:https://blog.csdn.net/jxysgzs/article/details/107778949

aws s3分片上传参考文档:https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/mpu-upload-object.html

重要::虽然我很菜,写的也不够好,但我不接受任何批评,本文仅供有需要的人参考及自己记录用。

前端部分

安装vue-simple-loader 

npm install vue-simple-uploader --save  本文使用0.7.6版本

main.js中

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

将vue-simple-uploader项目下,src文件夹中的common和components文件夹下的文件引入自己的项目

下载地址:https://github.com/simple-uploader/vue-uploader/tree/master/src

我的项目中引入位置,分别放在components/upload 和 utils/upload 文件夹下

   

前端代码,将大文件分片上传到本地,在上传成功的回调onFileSuccess中,将本地文件上传到S3存储桶

<template>
    <div class="uploader">
        <!-- autoStart 需要设置成 false -->
        <uploader :options="options" :autoStart="false"
            :fileStatusText="{
                    success: '上传成功,等待后台处理...',
                    error: '上传失败',
                    uploading: '正在上传',
                    paused: '暂停上传',
                    waiting: '等待上传'
            }"
            @file-success="onFileSuccess"  @file-added="fileAdded"  @file-error="onFileError"
        ></uploader>
    </div>
</template>

<script>
    import uploader from '../../components/upload/uploader.vue'
    import {
        localFileToS3
    } from '@/api/file/file.js';
    
    export default {
        components: {
            uploader
        },
        data() {
            return {
                options: {
                    target: '/bigFileToLocal.do', // 目标上传 URL
                    chunkSize: 5 * 1024 * 1024, // 分块大小,要和后台合并的大小对应
                    singleFile: true, // 是否单文件
                    maxChunkRetries: 3, //最大自动失败重试上传次数
                    testChunks: false, //是否开启服务器分片校验, 默认true
                    query: { // 参数
                    },
                    headers: { // 请求头认证
                        "token": localStorage.getItem('token')
                    },
                }
            }
        },
        methods: {
            //大文件上传所需
            fileAdded(file) {
                //选择文件后暂停文件上传,上传时手动启动
                file.pause()
            },
            onFileError(file) {
                console.log('error', file)
            },
            onFileSuccess(rootFile, file, response, chunk) { // 文件上传到本地成功后的回调
                var res = JSON.parse(response);
                if (res.code == "200") {
                    // 上传成功,上传本地文件到s3
                    var fileName = res.obj.fileName;
                    var filePath = res.obj.filePath;
                    let params = {
                        fileName: fileName,
                        filePath: filePath
                    }
                    
                    localFileToS3(params).then(res => { // 底层是axios请求
                        // 将上传到本地的文件上传到AWS s3
                        console.log(res);
                    })
                }
            },
        },
    }
</script>

<style>
    .uploader {
        position: relative;
    }
</style>

后台部分 

步骤:

1. 接收vue-simple-loader分片传过来的参数,保存到本地项目目录下

2. 取得本地项目目录下的文件,分片上传到s3

3. 删除本地保存的文件

Controller部分

import com.systron.common.controller.BaseController;
import com.systron.common.utils.ResponseApi;
import com.systron.models.sys.Chunk;
import com.systron.service.sys.FileService;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Controller
public class UploadController {

    private Logger logger = LoggerFactory.getLogger(UploadController.class);

    @Autowired
    private FileService fileService;

    /**
     * 大文件分片上传后保存到本地项目目录
     *
     * @param chunk
     * @param request
     * @param response
     */
    @RequestMapping(value="/bigFileToLocal.do")
    public void bigFileToLocal(@ModelAttribute Chunk chunk, HttpServletRequest request, HttpServletResponse response) {
        ResponseApi<Object> responseApi = new ResponseApi<Object>();

        // 分片上传
        responseApi = fileService.bigFileToLocal(chunk);
        if (null != responseApi && StringUtils.isNotEmpty(responseApi.getCode())) {
            response.setStatus(Integer.valueOf(responseApi.getCode()));
        } else {
            response.setStatus(201);
        }
        outObjectToJson(response, responseApi);
    }

    /**
     * 本地大文件分片上传到s3存储桶
     * @param request
     * @param response
     */
    @RequestMapping(value="/localFileToS3.do")
    public void localFileToS3(HttpServletRequest request, HttpServletResponse response) {
        ResponseApi<Object> responseApi = new ResponseApi<Object>();

        String allFilePath = request.getParameter("filePath"); // 文件路径
        String fileName = request.getParameter("fileName"); // 文件名称
        
        responseApi = fileService.localFileToS3(fileName, allFilePath);
        outObjectToJson(response, responseApi);
    }
}

Service部分 

import com.alibaba.fastjson.JSONObject;
import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.TransferManagerBuilder;
import com.amazonaws.services.s3.transfer.Upload;
import com.systron.common.utils.ResponseApi;
import com.systron.common.utils.cache.CacheConfigUtil;
import com.systron.dao.sys.FileDao;
import com.systron.models.sys.Chunk;
import com.systron.utils.HelpUtil;
import com.systron.utils.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;

@Service
public class FileService {

    private Logger logger = LoggerFactory.getLogger(FileService.class);

    @Resource(name = "fileDao")
    private FileDao fileDao;

    // 存储桶名称
    private static String bucketName = CacheConfigUtil.getProperty("bucket.name");

    /**
     * 大文件分片上传到本地项目下
     * @param chunk 每个块信息
     * @return
     */
    public ResponseApi<Object> bigFileToLocal(Chunk chunk) {
        ResponseApi<Object> responseApi = new ResponseApi<Object>();
        /**
         * 每一个上传块都会包含如下分块信息:
         * chunkNumber: 当前块的次序,第一个块是 1,注意不是从 0 开始的。
         * totalChunks: 文件被分成块的总数。
         * chunkSize: 分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大。
         * currentChunkSize: 当前块的大小,实际大小。
         * totalSize: 文件总大小。
         * identifier: 这个就是每个文件的唯一标示。
         * filename: 文件名。
         * relativePath: 文件夹上传的时候文件的相对路径属性。
         * 一个分块可以被上传多次,当然这肯定不是标准行为,但是在实际上传过程中是可能发生这种事情的,这种重传也是本库的特性之一。
         *
         * 根据响应码认为成功或失败的:
         * 200 文件上传完成
         * 201 文加快上传成功
         * 500 第一块上传失败,取消整个文件上传
         * 507 服务器出错自动重试该文件块上传
         */
        String path = PathUtils.getFileDir();
        String fileName = chunk.getFilename();
        String allFilePath = path + "/" + fileName;
        File file = new File(path, fileName);
        // 第一个块,则新建文件
        if (chunk.getChunkNumber() == 1 && !file.exists()) {
            try {
                file.createNewFile();
            } catch (IOException e) {
                responseApi.setCode("500");
                responseApi.setMsg("exception:createFileException");
                return responseApi;
            }
        }
        // 进行写文件操作
        try (
                //将块文件写入文件中
                InputStream fos = chunk.getFile().getInputStream();
                RandomAccessFile raf = new RandomAccessFile(file, "rw")
        ) {
            int len = -1;
            byte[] buffer = new byte[1024];
            raf.seek((chunk.getChunkNumber() - 1) * 1024 * 1024 * 5);
            while ((len = fos.read(buffer)) != -1) {
                raf.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
            if (chunk.getChunkNumber() == 1) {
                file.delete();
            }
            responseApi.setCode("507");
            responseApi.setMsg("exception:writeFileException");
            return responseApi;
        }

        if (chunk.getChunkNumber().equals(chunk.getTotalChunks())) {
            // 保存到本地文件成功
            responseApi.setCode("200");
            responseApi.setMsg("over");
       // 返回文件路径和文件名称  JSONObject json
= new JSONObject(); json.put("fileName", fileName); json.put("filePath", allFilePath); responseApi.setObj(json); System.out.println(json); return responseApi; } else { responseApi.setCode("201"); responseApi.setMsg("ok"); return responseApi; } } /** * 本地大文件分片上传到s3存储桶 * @param fileName * @param allFilePath */ public ResponseApi<Object> localFileToS3(String fileName, String allFilePath) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); // 1. 前端上传的文件整合保存到本地成功,将本地文件分片上传到s3存储桶 String suffix = fileName.split("[.]")[1]; String url = ""; responseApi = awsLocalFileToS3(fileName, allFilePath); if ("200".equals(responseApi.getCode())) { // 2. 上传到s3成功后,获取返回url url = String.valueOf(responseApi.getObj()); // 3. 删除本地文件 boolean fileDelFlag = HelpUtil.delete(allFilePath); if (!fileDelFlag) { logger.info("删除本地文件失败,文件路径:" + allFilePath); } } return responseApi; } /** * 本地大文件分片上传到s3存储桶 * 具体实现 * * @param fileName 文件名称 * @param path 文件路径 */ public ResponseApi<Object> awsLocalFileToS3(String fileName, String path) { ResponseApi<Object> responseApi = new ResponseApi<Object>(); Regions clientRegion = Regions.CN_NORTHWEST_1; try { AmazonS3 s3Client = AmazonS3ClientBuilder.standard() .withRegion(clientRegion) .withCredentials(new ProfileCredentialsProvider()) .build(); TransferManager tm = TransferManagerBuilder.standard() .withS3Client(s3Client) .build(); String objectKey = System.currentTimeMillis() + "_" + Math.random() + "_" + fileName; Upload upload = tm.upload(bucketName, objectKey, new File(path)); logger.info("上传开始:" + fileName); // 上传完成 upload.waitForCompletion(); logger.info("上传完成:" + fileName); String url = "https://" + bucketName + ".s3.cn-northwest-1.amazonaws.com.cn/" + objectKey; responseApi.setCode("200"); responseApi.setMsg("ok"); responseApi.setObj(url); return responseApi; } catch (AmazonServiceException e) { // The call was transmitted successfully, but Amazon S3 couldn't process // it, so it returned an error response. e.printStackTrace(); responseApi.setCode("508"); responseApi.setMsg("AmazonServiceException"); return responseApi; } catch (SdkClientException e) { // Amazon S3 couldn't be contacted for a response, or the client // couldn't parse the response from Amazon S3. e.printStackTrace(); responseApi.setCode("508"); responseApi.setMsg("SdkClientException"); return responseApi; } catch (InterruptedException e) { e.printStackTrace(); responseApi.setCode("508"); responseApi.setMsg("InterruptedException"); return responseApi; } } }

获取服务器根路径

public class PathUtils {

    /**
     * 获取服务器存放文件的目录路径
     *
     * @return 目录路径(String)
     */
    public static String getFileDir() {
        String path = ClassUtils.getDefaultClassLoader().getResource("").getPath().substring(1) + "static/file";
        File dir = new File(path);
        if (!dir.exists()) {
            dir.mkdirs();
        }
        return path;
    }
}

删除本地文件HelpUtil中的delete方法

/**
 * 删除文件
 *
 * @param fileName 待删除的完整文件名
 * @return
 */
public static boolean delete(String fileName) {
    boolean result = false;
    File f = new File(fileName);
    if (f.exists()) {
        result = f.delete();
    } else {
        result = true;
    }
    return result;
}

其他

ResponseApi帮助类,返回结果

/**
 * 返回结果类
 */
public class ResponseApi<T> {
    private String code;
    private String msg;
    private T obj; 
 
    public ResponseApi() {  
        code = "0000";
        msg = "成功";  
    }
    public ResponseApi(T obj) { 
       super();
       code = "0000";
       msg = "成功";  
       this.obj = obj; 
    }
    public ResponseApi(String code,String msg, T obj) { 
        super();
        this.code = code;
        this.msg = msg;  
        this.obj = obj; 
    }
    
    // getter/setter
    
}

 Chunk帮助类

/**
 * 文件块
 *
 */
public class Chunk implements Serializable {
    /**
     * 当前文件块,从1开始
     */
    private Integer chunkNumber;
    /**
     * 分块大小
     */
    private Long chunkSize;
    /**
     * 当前分块大小
     */
    private Long currentChunkSize;
    /**
     * 总大小
     */
    private Long totalSize;
    /**
     * 文件标识
     */
    private String identifier;
    /**
     * 文件名
     */
    private String filename;
    /**
     * 相对路径
     */
    private String relativePath;
    /**
     * 总块数
     */
    private Integer totalChunks;

    /**
     * 二进制文件
     */
    private MultipartFile file;

    // getter/setter
}
原文地址:https://www.cnblogs.com/aiyowei/p/15763262.html