• elementUI大文件分片上传


    前端实现

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

    1. <el-form :model="ruleForm" ref="ruleForm" :label-width="formLabelWidth" :rules="theRules" >
    2. <el-form-item prop="jar" :label-width="formLabelWidth">
    3. <label slot="lable" style="font-weight: lighter">上传文件</label>
    4. <el-upload
    5. ref="upload"
    6. action=""
    7. :http-request="handleFile"
    8. :on-preview="handlePreview"
    9. :on-remove="handleRemove"
    10. :before-remove="beforeRemove"
    11. :on-change="handleChange"
    12. :multiple="false"
    13. :limit="1"
    14. :file-list="fileList"
    15. accept=".tar">
    16. <el-button slot="trigger" size="small" type="primary" :disabled="fileButtonDisabled">选择应用包</el-button>
    17. <el-button style="margin-left: 10px" size="small" type="success" @click="uploadFile" :disabled="fileButtonDisabled">上传</el-button>
    18. </el-upload>
    19. </el-form-item>
    20. <el-form-item>
    21. <label slot="lable" style="font-weight: lighter"></label>
    22. <el-progress :text-inside="true" :stroke-width="15" :percentage="filePercentage"></el-progress>
    23. </el-form-item>
    24. </el-form>
    25. <div slot="footer" class="dialog-footer">
    26. <el-button @click="handleClose('ruleForm')">取 消</el-button>
    27. <el-button type="primary" @click="submitForm('ruleForm')">确 定</el-button>
    28. </div>

    复制

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

    1. data: function () {
    2. return {
    3. ruileForms: {
    4. file: '',
    5. jar: ''
    6. }
    7. theRules: {
    8. //jar:[{required:true, message:"请上传tar包", trigger:'blur'}]
    9. }
    10. fileList:[],
    11. filePercentage: 0, // 文件上传进度条
    12. fileLocation: '', // 文件在后台方式的位置
    13. fileCancelUpload: false, // 取消文件分片上传
    14. fileButtonDisabled: false, //文件上传按钮禁用
    15. }
    16. }
    17. methods: {
    18. submitForm(formName) {
    19. this.$refs[formName].validate((valid) => {
    20. if (valid) {
    21. if (this.fileLocation == "") {
    22. this.$message({message:"请上传文件", type:'fail'})
    23. }
    24. let formData = new FormData()
    25. formData.append("fileLocation", this.fileLocation)
    26. // 设置header头
    27. let config = {
    28. headers: {
    29. 'Content-Type':'multipart/form-data',
    30. }
    31. }
    32. Axios.post('/api/fileUpload', formData, config)
    33. .then((response) => {
    34. if (response.data.result == true) {
    35. this.$message({message:"成功", type:'success'})
    36. this.resetForm('ruleForm')
    37. }
    38. })
    39. .catch((err) => {
    40. console.log(err)
    41. })
    42. }
    43. })
    44. },
    45. handleFile() {
    46. // 空方法
    47. },
    48. handleChange(file, fileList) {
    49. // 文件改变时
    50. this.fileList = fileList
    51. },
    52. handleRemove(file, fileList) {
    53. this.fileCancelUpload = true
    54. this.filePercentage = 0 //进度条置空
    55. this.fileList = []
    56. this.fileButtonDisabled = false // 上传可点击
    57. },
    58. beforeRemove(file, fileList) {
    59. return this.$confirm('确定移除 ${file.name} ?');
    60. },
    61. //上传文件
    62. uploadFile() {
    63. let file = this.fileList[0] ? this.fileList[0].raw : ""
    64. if (file == "") {//判断文件是否存在
    65. this.$message({message:"未选择文件", type:'fail'})
    66. return;
    67. }
    68. if (file.size > 50 * 1024 * 1024) {
    69. //判断文件大小
    70. this.$message({message:"文件不能大于50M", type:'fail'})
    71. return;
    72. }
    73. //判断文件类型
    74. if (file.type.indexOf("tar") == "-1") { //application/x-tar
    75. //判断文件大小
    76. this.$message({message:"文件必须为tar包", type:'fail'})
    77. return;
    78. }
    79. if (file.name.length > 30) {
    80. //判断文件大小
    81. this.$message({message:"文件名大于30个字符", type:'fail'})
    82. return;
    83. }
    84. this.fileButtonDisabled = true
    85. // 唯一标识
    86. var uiqueIdentifier = this.Id + '-' + parseInt(new Data().getTime() / 1000)
    87. console.log(uiqueIdentifier)
    88. this.uploadBySplit(file, uiqueIdentifier, 0)
    89. },
    90. //分片上传
    91. uploadBySplit(file, identifier, i) {
    92. //如果取消上传直接初始化为最初状态
    93. if (this.fileCancelUpload) {
    94. this.fileCancelUpload = false
    95. this.filePercentage = 0
    96. this.fileList = []
    97. this.fileButtonDisabled = false
    98. }
    99. var chunkSize = 1024 * 1024 * 1; //分片大小1M
    100. var size = file.size; //总大小
    101. var totalChunks = Math.ceil(size/chunkSize);
    102. //分片停止条件
    103. if (i == totalChunks) {
    104. this.$message({message:"上传成功", type:'success'})
    105. return;
    106. }
    107. //计算每一篇的起始位置和结束位置
    108. var start = i * chunkSize;
    109. var end = Math.min(size, start + chunkSize);
    110. var fileData= file.slice(start, end)
    111. //文件分块上传
    112. var reader = new FileReader();
    113. reader.readAsBinaryString(fileData);
    114. reader.onload = function(e) {
    115. let formData = new FormData();
    116. formData.append('chunkNumber', i+1); //当前第几片,从0开始,文件下表从1计算。
    117. formData.append('chunkSize', chunkSize); //当前分片大小
    118. formData.append('currentChunkSize', fileData.size); //当前块大小
    119. formData.append('totalSize', size) //总的大小
    120. formData.append('identifier', identifier) //唯一标识
    121. formData.append('filename', file.name) //文件名
    122. formData.append('type', file.type) //文件类型
    123. // formData.append('relativePath', "/") //相对路径,暂时没用
    124. formData.append('totalChunks', totalChunks) //总片数
    125. formData.append('file', fileData) //总片数
    126. // 必须用这个Promise包起来axios,不然有问题
    127. return new Promise((resolve, reject) => {
    128. // 必须用这个类型的头,并且要包括boundary
    129. let config = {
    130. headers: {
    131. 'Content-Type':'multipart/form-data; charset=utf-8; boundary="another cool boundary";',
    132. }
    133. }
    134. Axios.post('/api/upload', formData, config)
    135. .then((response) => {
    136. if (response.data.result == true) {
    137. if (response.data.data != "") {
    138. this.fileLocation=response.data.data//合并后文件放置的位置
    139. }
    140. //上传进度
    141. var process =Math.random(end/ size*1000);
    142. this.fileLocation = process
    143. i++;
    144. this.uploadBySplit(file, identifier, i);
    145. resolve(response.data)
    146. }else {
    147. this.$message({message:"分片上传失败", type:'fail'})
    148. reject(err)
    149. }
    150. })
    151. .catch((err) => {
    152. console.log(err)
    153. })
    154. })
    155. }
    156. }
    157. }

    后端实现 java版

    1. @ResponseBody
    2. @RequestMapping(value="/upload", method = RequestMethod.Post)
    3. public JsonResult upload(HttpServletRequest request, Chunk chunk) {
    4. try {
    5. boolen isMutipart = ServletFileUpload.isMutipartContent(request);
    6. if(isMutipart) {
    7. MultipartFile file = chunk.getFile();
    8. if(file == null) {
    9. return JsonResult.failure("文件为空");
    10. }
    11. File outFile = new File(generatePath(chunk));
    12. InputStream inputStream = file.getInputStream();
    13. FileUtils.copyInputStreamToFile(inputStream, outFile);
    14. //判断所有分片是否全部上传完成,完成就合并
    15. File dir = new File(generateFileDir(chunk));
    16. File[] files = dir.listFiles();
    17. if (files.length == chunk.getTotalChunks()) {
    18. String filePath = mergeFile(chunk); //合并文件
    19. return JsonResult.success(filePath);
    20. }
    21. }
    22. return JsonResult.success();
    23. } catch (Exception e) {
    24. log.error(e)
    25. return JsonResult.failure("系统错误");
    26. }
    27. }
    28. // 获取上传文件路径
    29. public String generatePath(Chunk chunk) {
    30. StringBuilder sb = new StringBuilder();
    31. sb.append(uploadDir).append("/").append(chunk.getIdentifier());
    32. if (!Files.isWritable(Paths.get(sb.toString()))) {
    33. log.info("path not exist, create path:" , sb.toString());
    34. }
    35. try {
    36. Files.createDiretories(Paths.get(sb.toString()));
    37. } catch (IOExeception e){
    38. log.error(e)
    39. }
    40. return sb.append("/").append(chunk.getFilename()).append("-").append(chunk.getChunkNumber().toString());
    41. }
    42. //获取切片路径
    43. public String getnerateFileDir(Chunk chunk) {
    44. StringBuilder sb = new StringBuilder();
    45. sb.append(uploadDir).append("/")/append(chunk.getIdentifier());
    46. return sb.toString();
    47. }
    48. //合并文件
    49. public String mergeFile(Chunk chunk) {
    50. String filename = chunk.getFilename(); //文件名
    51. String folder = generateFileDir(chunk); //文件路径
    52. String file = folder + File.separator + filename; //生成文件名
    53. merge(file, folder, filename); //合并
    54. return chunk.getIdentifier() + + File.separator + filename; //返回相对路径
    55. }
    56. //合并
    57. public static void merge(String targetFile, String folder) {
    58. try {
    59. Files.createFile(Paths.get(targetFile));
    60. Files.list(Paths.get(folder))
    61. .filter(path -> path.getFileName().toString().contains("-"))
    62. .sorted((o1, o2) -> {
    63. String p1 = o1.getFileName().toString();
    64. String p2 = o2.getFileName().toString();
    65. int i1 = p1.lastIndexOf("-");
    66. int i2 = p2.lastIndexOf("-");
    67. return Integer.valueOf(p2.substring(i2)).compareTo(Integer.valueOf(p1.substring(i1)));
    68. })
    69. .forEach(path -> {
    70. try {
    71. //以追加的形式写入文件
    72. Files.write(Paths.get(targetFile), Files.readAllBytes(path), StandardOpenOption.APPEND);
    73. //合并后删除该块
    74. Files.delete(path);
    75. } catch (IOException e) {
    76. e.printStackTrace();
    77. }
    78. });
    79. } catch (IOException e) {
    80. e.printStackTrace();
    81. }
    82. }

    复制

    chunk 块结构

    1. @Data
    2. @Entity
    3. public class Chunk implements Serializable {
    4. @Id
    5. @GeneratedValue
    6. private Long id;
    7. /**
    8. * 当前文件块,从1开始
    9. */
    10. @Column(nullable = false)
    11. private Integer chunkNumber;
    12. /**
    13. * 分块大小
    14. */
    15. @Column(nullable = false)
    16. private Long chunkSize;
    17. /**
    18. * 当前分块大小
    19. */
    20. @Column(nullable = false)
    21. private Long currentChunkSize;
    22. /**
    23. * 总大小
    24. */
    25. @Column(nullable = false)
    26. private Long totalSize;
    27. /**
    28. * 文件标识
    29. */
    30. @Column(nullable = false)
    31. private String identifier;
    32. /**
    33. * 文件名
    34. */
    35. @Column(nullable = false)
    36. private String filename;
    37. /**
    38. * 相对路径
    39. */
    40. @Column(nullable = false)
    41. private String relativePath;
    42. /**
    43. * 总块数
    44. */
    45. @Column(nullable = false)
    46. private Integer totalChunks;
    47. /**
    48. * 文件类型
    49. */
    50. @Column
    51. private String type;
    52. @Transient
    53. private MultipartFile file;
    54. }
  • 相关阅读:
    nodejs--开发自己的项目——2.1——注册用户信息
    图像处理: 马赛克艺术
    vue3 自定义组件 v-model 原理解析
    netty系列之:netty中的核心MessageToMessage编码器
    如何动态的测试Thrift服务
    使用await解决异步问题的注意点总结
    服务器搭建(TCP套接字)-epoll版(服务端)
    纯 CSS 实现 超长内容滚动播放。
    如何使用 Nginx 创建临时和永久重定向
    PyCharm安装教程
  • 原文地址:https://blog.csdn.net/lxw1005192401/article/details/128201698