大文件上传切片上传 vue java

霸气de小男生 提交于 2020-08-06 20:07:23

大文件上传

前端实现

使用vue+elementui进行前端开发, 实现在dialog中 带进度条的上传大文件页面

<el-form :model="ruleForm" ref="ruleForm" :label-width="formLabelWidth" :rules="theRules" >
    <el-form-item prop="jar" :label-width="formLabelWidth">
        <label slot="lable" style="font-weight: lighter">上传文件</label>
        <el-upload
            ref="upload"
            action=""
            :http-request="handleFile"
            :on-preview="handlePreview"
            :on-remove="handleRemove"
            :before-remove="beforeRemove"
            :on-change="handleChange"
            :multiple="false"
            :limit="1"
            :file-list="fileList"
            accept=".tar">
            <el-button slot="trigger" size="small" type="primary" :disabled="fileButtonDisabled">选择应用包</el-button>
            <el-button style="margin-left: 10px" size="small" type="success" @click="uploadFile" :disabled="fileButtonDisabled">上传</el-button>
        </el-upload>
    </el-form-item>

    <el-form-item>
        <label slot="lable" style="font-weight: lighter"></label>
        <el-progress :text-inside="true" :stroke-width="15" :percentage="filePercentage"></el-progress>
    </el-form-item>
</el-form>

<div slot="footer" class="dialog-footer">
    <el-button @click="handleClose('ruleForm')">取 消</el-button>
    <el-button type="primary" @click="submitForm('ruleForm')">确 定</el-button>
</div>

这里使用axios有一个坑需要注意一下,必须是这个指定的header,且必须用Promise包起来。

    data: function () {
        return {
            ruileForms: {
                file: '',
                jar: ''
            }
            theRules: {
                //jar:[{required:true, message:"请上传tar包", trigger:'blur'}]
            }
            fileList:[],
            filePercentage: 0, // 文件上传进度条
            fileLocation: '', // 文件在后台方式的位置
            fileCancelUpload: false, // 取消文件分片上传
            fileButtonDisabled: false, //文件上传按钮禁用
        }
    }
    methods: {
        submitForm(formName) {
            this.$refs[formName].validate((valid) => {
              if (valid) {
                  if (this.fileLocation == "") {
                      this.$message({message:"请上传文件", type:'fail'})
                  }

                  let formData = new FormData()
                  formData.append("fileLocation", this.fileLocation)

                  // 设置header头
                  let config = {
                      headers: {
                          'Content-Type':'multipart/form-data',
                      }
                  }

                  Axios.post('/api/fileUpload', formData, config)
                    .then((response) => {
                        if (response.data.result == true) {
                            this.$message({message:"成功", type:'success'})
                            this.resetForm('ruleForm')

                        }
                    })
                    .catch((err) => {
                        console.log(err)
                    })

              }

            })
        },

        handleFile() {
          // 空方法
        },
        handleChange(file, fileList) {
            // 文件改变时
            this.fileList = fileList
        },
        handleRemove(file, fileList) {
            this.fileCancelUpload = true
            this.filePercentage = 0 //进度条置空
            this.fileList = []
            this.fileButtonDisabled = false // 上传可点击
        },
        beforeRemove(file, fileList) {
            return this.$confirm('确定移除 ${file.name} ?');
        },

        //上传文件
        uploadFile() {
            let file = this.fileList[0] ? this.fileList[0].raw : ""
            if (file == "") {//判断文件是否存在
                 this.$message({message:"未选择文件", type:'fail'})
                return;
            }

             if (file.size > 50 * 1024 * 1024) {
                 //判断文件大小
                 this.$message({message:"文件不能大于50M", type:'fail'})
                return;
            }

            //判断文件类型
            if (file.type.indexOf("tar") == "-1") { //application/x-tar
                //判断文件大小
                this.$message({message:"文件必须为tar包", type:'fail'})
                return;
            }

            if (file.name.length > 30) {
                //判断文件大小
                this.$message({message:"文件名大于30个字符", type:'fail'})
                return;
            }

            this.fileButtonDisabled = true
            // 唯一标识
            var uiqueIdentifier = this.Id + '-' + parseInt(new Data().getTime() / 1000)
            console.log(uiqueIdentifier)
            this.uploadBySplit(file, uiqueIdentifier, 0)

        },
        //分片上传
        uploadBySplit(file, identifier, i) {

            //如果取消上传直接初始化为最初状态
            if (this.fileCancelUpload) {
                this.fileCancelUpload = false
                this.filePercentage = 0
                this.fileList = []
                this.fileButtonDisabled = false
            }

            var chunkSize = 1024 * 1024 * 1; //分片大小1M
            var size = file.size; //总大小
            var totalChunks = Math.ceil(size/chunkSize);

            //分片停止条件
            if (i == totalChunks) {
                this.$message({message:"上传成功", type:'success'})
                return;
            }

            //计算每一篇的起始位置和结束位置
            var start = i * chunkSize;
            var  end = Math.min(size, start + chunkSize);
            var fileData= file.slice(start, end)

            //文件分块上传
            var reader = new FileReader();
            reader.readAsBinaryString(fileData);
            reader.onload = function(e) {
                let formData = new FormData();
                formData.append('chunkNumber', i+1); //当前第几片,从0开始,文件下表从1计算。
                formData.append('chunkSize', chunkSize); //当前分片大小
                formData.append('currentChunkSize', fileData.size); //当前块大小
                formData.append('totalSize', size) //总的大小
                formData.append('identifier', identifier) //唯一标识
                formData.append('filename', file.name) //文件名
                formData.append('type', file.type) //文件类型
                // formData.append('relativePath', "/") //相对路径,暂时没用
                formData.append('totalChunks', totalChunks) //总片数
                formData.append('file', fileData) //总片数

                // 必须用这个Promise包起来axios,不然有问题
                return new Promise((resolve, reject) => {

                    // 必须用这个类型的头,并且要包括boundary
                    let config = {
                        headers: {
                            'Content-Type':'multipart/form-data; charset=utf-8; boundary="another cool boundary";',
                        }
                    }

                    Axios.post('/api/upload', formData, config)
                        .then((response) => {
                            if (response.data.result == true) {
                                if (response.data.data != "") {
                                    this.fileLocation=response.data.data//合并后文件放置的位置
                                }

                                //上传进度
                                var process =Math.random(end/ size*1000);
                                this.fileLocation = process
                                i++;
                                this.uploadBySplit(file, identifier, i);
                                resolve(response.data)
                            }else {
                                this.$message({message:"分片上传失败", type:'fail'})
                                reject(err)
                            }
                        })
                        .catch((err) => {
                            console.log(err)
                        })

                })
            }
        }



    }

后端实现 java版

    @ResponseBody
    @RequestMapping(value="/upload", method = RequestMethod.Post)
    public JsonResult upload(HttpServletRequest request, Chunk chunk) {
        try {
            
            boolen isMutipart = ServletFileUpload.isMutipartContent(request);
            if(isMutipart) {
                MultipartFile file = chunk.getFile();
                
                if(file == null) {
                    return JsonResult.failure("文件为空");
                }
                
                File outFile = new File(generatePath(chunk));
                InputStream inputStream = file.getInputStream();
                FileUtils.copyInputStreamToFile(inputStream, outFile);
                
                //判断所有分片是否全部上传完成,完成就合并
                File dir = new File(generateFileDir(chunk));
                File[] files = dir.listFiles();
                if (files.length == chunk.getTotalChunks()) {
                    String filePath = mergeFile(chunk); //合并文件
                    return JsonResult.success(filePath);
                }
                
            }
            
            
            return JsonResult.success();
        } catch (Exception e) {
            log.error(e)
            return JsonResult.failure("系统错误");
        }
    }
    
    // 获取上传文件路径
    public String generatePath(Chunk chunk) {
        StringBuilder sb = new StringBuilder();
        sb.append(uploadDir).append("/").append(chunk.getIdentifier());
        if (!Files.isWritable(Paths.get(sb.toString()))) {
            log.info("path not exist, create path:" , sb.toString());
        }
        try {
            Files.createDiretories(Paths.get(sb.toString()));
        } catch (IOExeception e){
            log.error(e)
        }
        
        return sb.append("/").append(chunk.getFilename()).append("-").append(chunk.getChunkNumber().toString());
    }
    
    //获取切片路径
    public String getnerateFileDir(Chunk chunk) {
         StringBuilder sb = new StringBuilder();
         sb.append(uploadDir).append("/")/append(chunk.getIdentifier());
         return sb.toString();
    }
    
    //合并文件
    public String mergeFile(Chunk chunk) {
        String filename = chunk.getFilename(); //文件名
        String folder = generateFileDir(chunk); //文件路径
        String file = folder + File.separator + filename; //生成文件名
        merge(file, folder, filename); //合并
        return chunk.getIdentifier() + + File.separator + filename; //返回相对路径
    }
    
    //合并
    public static void merge(String targetFile, String folder) {
        try {
            Files.createFile(Paths.get(targetFile));
            Files.list(Paths.get(folder))
                    .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(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
                            //合并后删除该块
                            Files.delete(path);
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    

chunk 块结构

@Data
@Entity
public class Chunk implements Serializable {
    @Id
    @GeneratedValue
    private Long id;
    /**
     * 当前文件块,从1开始
     */
    @Column(nullable = false)
    private Integer chunkNumber;
    /**
     * 分块大小
     */
    @Column(nullable = false)
    private Long chunkSize;
    /**
     * 当前分块大小
     */
    @Column(nullable = false)
    private Long currentChunkSize;
    /**
     * 总大小
     */
    @Column(nullable = false)
    private Long totalSize;
    /**
     * 文件标识
     */
    @Column(nullable = false)
    private String identifier;
    /**
     * 文件名
     */
    @Column(nullable = false)
    private String filename;
    /**
     * 相对路径
     */
    @Column(nullable = false)
    private String relativePath;
    /**
     * 总块数
     */
    @Column(nullable = false)
    private Integer totalChunks;
    /**
     * 文件类型
     */
    @Column
    private String type;
    @Transient
    private MultipartFile file;
}

参考

HTML5结合springboot带进度条大文件分段上传

javascript之大文件分段上传、断点续传(一)

SpringBoot+Vue.js前后端分离实现大文件分块上传

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!