Spring boot + vue-simple-uploader实现分块上传,以及实现秒传

首先安装vue-simple-uploader和spark MD5

npm install vue-simple-uploader --save

npm i spark MD5

然后在main.js里面添加

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

新建一个FileUpload.vue组件

<template>
    <uploader :options="options" :autoStart="false"
              @file-success="onFileSuccess"
              @file-added="filesAdded">
        <uploader-unsupport></uploader-unsupport>
        <uploader-drop>
            <uploader-btn>选择文件</uploader-btn>
        </uploader-drop>
        <uploader-list></uploader-list>
    </uploader>
</template>


<script>
    import SparkMD5 from 'spark-md5'
    export default {
        name: "FileUpload",
        data(){
            return {
                options: {
                    target: '/erp/file_upload/chunk',//SpringBoot后台接收文件夹数据的接口
                    testChunks: true,//是否分片-分片
                    chunkSize: 30 * 1024 * 1024,//分块大小
                    fileParameterName: 'file',
                    maxChunkRetries: 3,  //最大自动失败重试上传次数
                    checkChunkUploadedByResponse: function (chunk, message) {
                        let objMessage = JSON.parse(message);
                        // 获取当前的上传块的集合
                        let chunkNumbers = objMessage.result.chunkNumbers;
                        // 判断当前的块是否被该集合包含,从而判定是否需要跳过
                        return (chunkNumbers || []).indexOf(chunk.offset + 1) >= 0;
                    }
                }
            }

        },methods:{
            onFileSuccess: function (rootFile, file, response, chunk) {
                let res = JSON.parse(response);
                if(res.code == "error"){
                    _this.$message({
                        message: res.message,
                        type: 'error'
                    });
                }
                //文件以存在,直接调用回调函数
                if (res.result.code == 200) {
                   //执行回调函数
                    this.$emit('doneBlock',res);
                }
                // 需要合并
                if (res.result.code == 205) {
                    const formData = new FormData();
                    formData.append("identifier", file.uniqueIdentifier);
                    formData.append("filename", file.name);
                    formData.append("totalSize", file.size);
                    this.$axios.post("/erp/file_upload/merge", formData).then((res) => {
                         //执行回调函数
                        this.$emit('doneBlock',res.data);
                    })
                }
            },filesAdded(file, event){
                this.computeMD5(file)
            },computeMD5(file) {
                const loading = this.$loading({
                    lock: true,
                    text: '正在计算MD5',
                    spinner: 'el-icon-loading',
                    background: 'rgba(0, 0, 0, 0.7)'
                });
                let fileReader = new FileReader();
                let time = new Date().getTime();
                let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
                let currentChunk = 0;
                const chunkSize = 30 * 1024 * 1024;
                let chunks = Math.ceil(file.size / chunkSize);
                let spark = new SparkMD5.ArrayBuffer();
                file.pause();
                loadNext();
                fileReader.onload = (e => {
                    spark.append(e.target.result);
                    if (currentChunk < chunks) {
                        currentChunk++;
                        loadNext();
                        // 实时展示MD5的计算进度
                        this.$nextTick(() => {
                            console.log('校验MD5 '+ ((currentChunk/chunks)*100).toFixed(0)+'%')
                        })
                    } else {
                        let md5 = spark.end();
                        loading.close();
                        this.computeMD5Success(md5, file);
                        console.log(`MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${new Date().getTime() - time} ms`);
                    }
                });
                fileReader.onerror = function () {
                    this.error(`文件${file.name}读取出错,请检查该文件`);
                    loading.close();
                    file.cancel();
                };
                function loadNext() {
                    let start = currentChunk * chunkSize;
                    let end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
                    fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
                }
            },computeMD5Success(md5, file) {
                file.uniqueIdentifier = md5;//把md5值作为文件的识别码
                file.resume();//开始上传
            }
        }
    }
</script>

在需要调用的页面加入代码:
HTML代码如下

<el-button type="primary" icon="el-icon-upload" @click="uploadFileVisible = true">上传</el-button>

 <el-dialog title="上传资料" :visible.sync="uploadFileVisible" width="600px">
      <file-upload style="height: 300px" @doneBlock="uploadSuccess"></file-upload>
  </el-dialog>

JS代码如下

//导入我们刚刚新建的组件
import FileUpload from './group/FileUpload.vue';
export default {
        name: "Test",
        components: {FileUpload},
        data() {
            return {
             uploadFileVisible:false
            }
        },methods: {
            uploadFile(){
                this.uploadFileVisible = true;
            },uploadSuccess(obj){
                //这里是上传成功后回调的函数,可以根据自己的业务需求来进行操作
                //我这边上传成功后往数据库插入一条数据
                var proFile ={};
                proFile.fileName = obj.result.fileName;
                proFile.recordDate = "";
                proFile.recorder = localStorage.getItem('ms_username');
                proFile.fileUrl = obj.result.fileUrl;
                proFile.fileSize = obj.result.fileSize;
                //保存的业务接口
                this.$axios.post("/erp/test/save", proFile).then((res) => {
                    if (res.data.code == "error") {
                        this.$message({
                            message: res.data.msg,
                            type: 'error'
                        });
                    } else {
                        this.$message({
                            message: res.data.msg,
                            type: 'success'
                        });
                        this.getData();
                    }
                })
            }
        }
    }

接下来是后台代码
新建两个实体类,ErpFiles,ErpChunk

ErpFiles.java

package com.example.entity;

import java.io.Serializable;
import java.util.Date;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;

@Entity
@Table(name = "erp_files")
@GenericGenerator(name = "jpa-uuid", strategy = "uuid")
public class ErpFiles implements Serializable{
    /**
     * 
     */
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(generator = "jpa-uuid")
    private String id;

    private String fileName;

    private String fileType;

    private String fileUrl;

    private Date creationTime;

    private String userName;

    private Long fileSize;
    
    private String fileMd5;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileType() {
        return fileType;
    }

    public void setFileType(String fileType) {
        this.fileType = fileType;
    }

    public String getFileUrl() {
        return fileUrl;
    }

    public void setFileUrl(String fileUrl) {
        this.fileUrl = fileUrl;
    }

    public Date getCreationTime() {
        return creationTime;
    }

    public void setCreationTime(Date creationTime) {
        this.creationTime = creationTime;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public Long getFileSize() {
        return fileSize;
    }

    public void setFileSize(Long fileSize) {
        this.fileSize = fileSize;
    }

    public String getFileMd5() {
        return fileMd5;
    }

    public void setFileMd5(String fileMd5) {
        this.fileMd5 = fileMd5;
    }
    
    
}

ErpChunk.java

package com.example.entity;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;

import org.hibernate.annotations.GenericGenerator;
import org.springframework.web.multipart.MultipartFile;
@Entity
@Table(name = "erp_chunk")
@GenericGenerator(name = "jpa-uuid", strategy = "uuid")
public class ErpChunk implements Serializable {
    private static final long serialVersionUID = 7073871700302406420L;

    @Id
    @GeneratedValue(generator = "jpa-uuid")
    private String id;
    /**
     * 当前文件块,从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 String type;

    /**
     * 要上传的文件
     */
    @Transient
    private MultipartFile file;
    
    @Transient
    private String parentId;

    
    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Integer getChunkNumber() {
        return chunkNumber;
    }

    public void setChunkNumber(Integer chunkNumber) {
        this.chunkNumber = chunkNumber;
    }

    public Long getChunkSize() {
        return chunkSize;
    }

    public void setChunkSize(Long chunkSize) {
        this.chunkSize = chunkSize;
    }

    public Long getCurrentChunkSize() {
        return currentChunkSize;
    }

    public void setCurrentChunkSize(Long currentChunkSize) {
        this.currentChunkSize = currentChunkSize;
    }

    public Long getTotalSize() {
        return totalSize;
    }

    public void setTotalSize(Long totalSize) {
        this.totalSize = totalSize;
    }

    public String getIdentifier() {
        return identifier;
    }

    public void setIdentifier(String identifier) {
        this.identifier = identifier;
    }

    public String getFilename() {
        return filename;
    }

    public void setFilename(String filename) {
        this.filename = filename;
    }

    public String getRelativePath() {
        return relativePath;
    }

    public void setRelativePath(String relativePath) {
        this.relativePath = relativePath;
    }

    public Integer getTotalChunks() {
        return totalChunks;
    }

    public void setTotalChunks(Integer totalChunks) {
        this.totalChunks = totalChunks;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public MultipartFile getFile() {
        return file;
    }

    public void setFile(MultipartFile file) {
        this.file = file;
    }

    public String getParentId() {
        return parentId;
    }
}

然后新建一个FileUploadController.java的文件

package com.example.controller;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

import com.example.entity.ErpChunk;
import com.example.entity.ErpFiles;
import com.example.service.ErpChunkService;
import com.example.service.ErpFilesService;
import com.example.utils.GooidServerSay;

@Controller
@RequestMapping("/file_upload")
public class FileUploadController {
    @Value("${file.uploadFolder1}")
    private String uploadFolder;
    @Value("${file.staticAccessPath1}")
    private String staticAccessPath;

    @Autowired
    private ErpChunkService chunkService;

    @Autowired
    private ErpFilesService filesService;

    /**
     * 验证当前文件块是否上传
     * 
     * @param chunk
     * @return
     */
    @RequestMapping(value = "/chunk", method = RequestMethod.GET)
    @ResponseBody
    public GooidServerSay checkChunks(ErpChunk chunk) {
        GooidServerSay ret = new GooidServerSay();
        try {
            String identifier = chunk.getIdentifier();
            Map<String, Object> params = new HashMap<String, Object>();
            params.put("EQ_identifier", identifier);
            List<ErpChunk> erpChunks = chunkService.find(params);
            Set<Integer> chunkNumbers = new HashSet<Integer>();
            for (ErpChunk erpChunk : erpChunks) {
                chunkNumbers.add(erpChunk.getChunkNumber());

            }
            Map<String, Object> res = new HashMap<String, Object>();
            res.put("chunkNumbers", chunkNumbers);
            params = new HashMap<String, Object>();
            params.put("EQ_fileMd5", identifier);
            List<ErpFiles> files = filesService.find(params);
            if (files.size() > 0) {
                res.put("code", 200);
                res.put("fileName", files.get(0).getFileName());
                res.put("fileUrl", files.get(0).getFileUrl());
                res.put("fileSize", files.get(0).getFileSize());
            }else if (erpChunks.size() > 0 && erpChunks.size() == erpChunks.get(0).getTotalChunks()) {
                res.put("message", "上传成功!");
                res.put("code", 205);
            }
            ret.setResult(res);
        } catch (Exception e) {
            ret.setCode("error");
            ret.setMsg("操作失败\n\n详细信息:" + e.getMessage());
            e.printStackTrace();
        }
        return ret;
    }

    /**
     * 分块上传
     * 
     * @param chunk
     * @return
     */
    /**
     * @param chunk
     * @return
     */
    @RequestMapping(value = "/chunk", method = RequestMethod.POST)
    @ResponseBody
    public GooidServerSay saveChunk(ErpChunk chunk) {
        GooidServerSay ret = new GooidServerSay();
        try {
            // 这里的操作和保存单段落的基本是一致的~
            MultipartFile file = chunk.getFile();
            String identifier = chunk.getIdentifier();
            byte[] bytes;
            try {
                bytes = file.getBytes();
                // 这里的不同之处在于这里进行了一个保存分块时将文件名的按照-chunkNumber的进行保存
                Path path = Paths
                        .get(generatePath(uploadFolder + LoginController.getUser().getUsername() + "/", chunk));
                Files.write(path, bytes);
            } catch (IOException e) {
                e.printStackTrace();
            }
            if (chunk.getFilename().lastIndexOf(".") > 0) {
                chunk.setType(chunk.getFilename().substring(chunk.getFilename().lastIndexOf(".")));
            }
            chunkService.save(chunk);
            // 这里进行的是保存到redis,并返回集合的大小的操作
            Map<String, Object> params = new HashMap<String, Object>();
            params.put("EQ_identifier", identifier);
            int chunks = chunkService.find(params).size();
            Map<String, Object> result = new HashMap<>();
            // 如果集合的大小和totalChunks相等,判定分块已经上传完毕,进行merge操作
            if (chunks == chunk.getTotalChunks().intValue()) {
                result.put("message", "上传成功!");
                result.put("code", 205);
            }

            ret.setResult(result);
        } catch (Exception e) {
            e.printStackTrace();
            ret.setCode("error");
            ret.setMsg("操作失败\n\n详细信息:" + e.getMessage());
        }
        return ret;
    }

    /**
     * 生成分块的文件路径
     */
    private static String generatePath(String uploadFolder, ErpChunk chunk) {
        StringBuilder sb = new StringBuilder();
        // 拼接上传的路径
        sb.append(uploadFolder).append(File.separator).append(chunk.getIdentifier());
        // 判断uploadFolder/identifier 路径是否存在,不存在则创建
        if (!Files.isWritable(Paths.get(sb.toString()))) {
            try {
                Files.createDirectories(Paths.get(sb.toString()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        // 返回以 - 隔离的分块文件,后面跟的chunkNumber方便后面进行排序进行merge
        return sb.append(File.separator).append(chunk.getFilename()).append("-").append(chunk.getChunkNumber())
                .toString();

    }

    @RequestMapping(value = "/merge", method = RequestMethod.POST)
    @ResponseBody
    public GooidServerSay mergeChunks(ErpChunk chunk) {
        GooidServerSay ret = new GooidServerSay();
        try {
            // 如果合并后的路径不存在,则新建
            if (!Files.isWritable(Paths.get(uploadFolder + LoginController.getUser().getUsername() + "/"))) {
                Files.createDirectories(Paths.get(uploadFolder + LoginController.getUser().getUsername() + "/"));
            }
            // 合并的文件名
            String target = uploadFolder + LoginController.getUser().getUsername() + "/" + File.separator
                    + chunk.getIdentifier()
                    + (chunk.getFilename().lastIndexOf(".") > 0
                            ? chunk.getFilename().substring(chunk.getFilename().lastIndexOf("."))
                            : "");
            File file = new File(target);
            if (!file.exists()) {
                // 创建文件
                Files.createFile(Paths.get(target));
                // 遍历分块的文件夹,并进行过滤和排序后以追加的方式写入到合并后的文件
                Files.list(Paths.get(uploadFolder + LoginController.getUser().getUsername() + "/" + File.separator
                        + chunk.getIdentifier()))
                        // 过滤带有"-"的文件
                        .filter(path -> path.getFileName().toString().contains("-"))
                        // 按照从小到大进行排序
                        .sorted((o1, o2) -> {
                            String p1 = o1.getFileName().toString();
                            String p2 = o2.getFileName().toString();
                            int i1 = p1.lastIndexOf("-");
                            int i2 = p2.lastIndexOf("-");
                            return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
                        }).forEach(path -> {
                            try {
                                // 以追加的形式写入文件
                                Files.write(Paths.get(target), Files.readAllBytes(path), StandardOpenOption.APPEND);
                                // 合并后删除该块
                                Files.delete(path);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        });
                File file1 = new File(uploadFolder + LoginController.getUser().getUsername() + "/" + File.separator
                        + chunk.getIdentifier());
                if (!file1.exists()) {
                    file1.delete();
                }
            }

                        
            ErpFiles files = new ErpFiles();
            files.setFileName(chunk.getFilename());
            files.setCreationTime(new Date());
            files.setFileMd5(chunk.getIdentifier());
            files.setFileSize(chunk.getTotalSize());
            if (chunk.getFilename().lastIndexOf(".") > 0) {
                files.setFileType(chunk.getFilename().substring(chunk.getFilename().lastIndexOf(".")));
            }
            files.setFileUrl("/erp/swagger/uploadFiles1/" + LoginController.getUser().getUsername() + "/"
                    + chunk.getIdentifier() + chunk.getType());
            files.setUserName(LoginController.getUser().getUsername());
            filesService.save(files);
            Map<String, String> result = new HashMap<String, String>();
            result.put("fileName", chunk.getFilename());
            result.put("fileUrl", files.getFileUrl());
            result.put("fileSize", chunk.getTotalSize() + "");
            ret.setResult(result);
        } catch (IOException e) {
            e.printStackTrace();
            ret.setCode("error");
            ret.setMsg("操作失败\n\n详细信息:" + e.getMessage());
        }
        return ret;
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,372评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,368评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,415评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,157评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,171评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,125评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,028评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,887评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,310评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,533评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,690评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,411评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,004评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,812评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,693评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,577评论 2 353