前端大文件分片MinIO上传的详细代码

大文件分片上传是一种将大文件拆分成多个小文件片段进行上传的技术。这种方式可以提高上传效率,减少网络传输时间,并且在网络不稳定或者上传过程中出现错误时,可以更容易地恢复上传进度。

大文件分片上传的步骤如下:

  • 将大文件分成多个固定大小的片段,通常每个片段的大小在几十KB到几MB之间。
  • 逐个上传每个文件片段,可以使用HTTP、FTP等协议进行传输。
  • 服务器接收到每个文件片段后,判断其MD5值进行保存或者合并操作。
  • 在上传过程中,通过MD5值维护一个上传进度记录,标记已经上传成功的文件片段,以便在上传中断后能够恢复上传进度。
  • 当所有文件片段都上传完成后,服务器将文件片段进行合并,得到完整的大文件。

大文件分片上传的好处有:

  • 提高上传速度:将大文件拆分成小片段,可以同时上传多个片段,从而提高上传速度。
  • 断点续传:如果在上传过程中发生中断或者错误,可以根据上传进度记录,只重新上传丢失或者出错的文件片段,从而减少网络传输时间。
  • 易于管理:将大文件拆分成小片段,可以更方便地管理和存储,避免了一次性上传整个大文件可能导致的内存占用问题。

大文件分片上传技术已经广泛应用于各种云存储、文件传输等领域,为用户提供了更好的上传体验和效率。

视图代码 

 大文件分片需要读取时间所以要给加载状态,下面例子只适合单文件上传且带上传进度展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="slice-upload">
     
       
        选择文件
         
  
    <div class="el-upload__tip">
      请上传不超过 <span style="color: #e6a23c">{{ maxCalc }}</span> 的文件
    </div>
    <div class="file-list">
      <div class="list-item">
          <i class="el-icon-document mr5"></i>
          <span>{{ file.name }}
            <em style="color: #67c23a">上传中....</em></span>
          <span class="percentage">{{ percentage }}%</span>
           
</div>
       
</div>
  </div>

逻辑代码

需要引入Md5 

1
npm install spark-md5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
import SparkMD5 from "spark-md5";
import axios from "axios";
import {
  getImagecheckFile,//检验是否上传过用于断点续传
  Imageinit,//用分片换取minIo上传地址
  Imagecomplete,//合并分片
} from "/*接口地址*/";
export default {
  name: "sliceUpload",
  /**
   * 外部数据
   * @type {Object}
   */
  props: {
    /**
     * @Description
     * 代码注释说明
     * 接口url
     * @Return
     */
    findFileUrl: String,
    continueUrl: String,
    finishUrl: String,
    removeUrl: String,
    /**
     * @Description
     * 代码注释说明
     * 最大上传文件大小 100G
     * @Return
     */
    maxFileSize: {
      type: Number,
      default: 100 * 1024 * 1024 * 1024,
    },
    /**
     * @Description
     * 代码注释说明
     * 切片大小
     * @Return
     */
    sliceSize: {
      type: Number,
      default: 50 * 1024 * 1024,
    },
    /**
     * @Description
     * 代码注释说明
     * 是否可以上传
     * @Return
     */
    show: {
      type: Boolean,
      default: true,
    },
    accept: String,
  },
  /**
   * 数据定义
   * @type {Object}
   */
  data() {
    return {
      /**
       * @Description
       * 代码注释说明
       * 文件
       * @Return
       */
      file: null,//源文件
      imageSize: 0,//文件大小单位GB
      uploadId: "",//上传id
      fullPath: "",//上传地址
      uploadUrls: [],//分片上传地址集合
      hash: "",//文件MD5
      /**
       * @Description
       * 代码注释说明
       * 分片文件
       * @Return
       */
      formDataList: [],
      /**
       * @Description
       * 代码注释说明
       * 未上传分片
       * @Return
       */
      waitUpLoad: [],
      /**
       * @Description
       * 代码注释说明
       * 未上传个数
       * @Return
       */
      waitNum: NaN,
      /**
       * @Description
       * 代码注释说明
       * 上传大小限制
       * @Return
       */
      limitFileSize: false,
      /**
       * @Description
       * 代码注释说明
       * 进度条
       * @Return
       */
      percentage: 0,
      percentageFlage: false,
      /**
       * @Description
       * 代码注释说明
       * 读取loading
       * @Return
       */
      loading: false,
      /**
       * @Description
       * 代码注释说明
       * 正在上传中
       * @Return
       */
      uploading: false,
      /**
       * @Description
       * 代码注释说明
       * 暂停上传
       * @Return
       */
      stoped: false,
 
      /**
       * @Description
       * 代码注释说明
       * 上传后的文件数据
       * @Return
       */
      fileData: {
        id: "",
        path: "",
      },
    };
  },
  /**
   * 数据监听
   * @type {Object}
   */
  watch: {
    //监控上传进度
    waitNum: {
      handler(v, oldVal) {
        let p = Math.floor(
          ((this.formDataList.length - v) / this.formDataList.length) * 100
        );
        // debugger;
        this.percentage = p > 100 ? 100 : p;
      },
      deep: true,
    },
    show: {
      handler(v, oldVal) {
        if (!v) {
          this.file = null
        }
      },
      deep: true,
    },
  },
  /**
   * 方法集合
   * @type {Object}
   */
  methods: {
    /**
     * 代码注释说明
     * 内存过滤器
     * @param  {[type]} ram [description]
     * @return {[type]}     [description]
     */
    ramFilter(bytes) {
      if (bytes === 0) return "0";
      var k = 1024;
      let sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
       let i = Math.floor(Math.log(bytes) / Math.log(k));
      return (bytes / Math.pow(k, i)).toFixed(2) + " " + sizes[i];
    },
    /**
     * 触发上传 文件处理
     * @param e
     */
    async choseFile(e) {
      const fileInput = e.target.files[0]; // 获取当前文件
      this.imageSize = this.ramFilter(fileInput.size);//记录文件大小
      if (!fileInput && !this.show) {
        return;
      }
      const pattern = /[u4e00-u9fa5]/;
      if (pattern.test(fileInput?.name)) {
        this.$message.warning("请不要上传带有中文名称的镜像文件!");
        return;
      }
 
      this.file = fileInput; // file 丢全局方便后面用 可以改进为func传参形式
      this.percentage = 0;
      if (this.file.size < this.maxFileSize) {
        this.loading = true;
        const FileSliceCap = this.sliceSize; // 分片字节数
        let start = 0; // 定义分片开始切的地方
        let end = 0; // 每片结束切的地方a
        let i = 0; // 第几片
        this.formDataList = []; // 分片存储的一个池子 丢全局
        this.waitUpLoad = []; // 分片存储的一个池子 丢全局
        while (end  {
            if (res) {
              this.hash = res;
              //console.log("拿到了:", res);
              // this.UploadStatus = `文件读取成功(${res}),文件上传中...`;
              //通过Md5值查询是否上传过
              getImagecheckFile({ fileCode: res }).then(
                (res2) => {
                  this.loading = false;
                  /**
                   * @Description
                   * 代码注释说明
                   * 全部切完以后 发一个请求给后端 拉当前文件后台存储的切片信息 用于检测有多少上传成功的切片
                   * fileUrl:有地址就是秒传因为已经存在该文件了
                   * shardingIndex:返回哪些已经上传用于断点续传
                   * @Return
                   */
                  let { fileUrl, shardingIndex } = res2.data.data; //检测是否上传过
                  if (!fileUrl) {
                    /**
                     * @Description
                     * 代码注释说明
                     * 当是断点续传时候
                     * 记得处理一下当前是默认全都没上传过暂不支持断点续传后端无法返回已上传数据如果你家后端牛一点可以在此处理断点续传
                     * @Return
                     */
                    this.waitUpLoad = this.formDataList;//当前是默认全都没上传过断点续传需处理
                    this.getFile()
                  } else {
                    // debugger;
                    this.formDataList = [{ key: fileUrl }];
                    this.waitNum = 1;
                    this.waitUpLoad = []; // 秒传则没有需要上传的切片
                    this.$message.success("文件已秒传");
                    this.$emit("fileinput", {
                      url: fileUrl,
                      code: this.hash,
                      imageSize: this.imageSize,
                    });
                    this.waitNum = 0;
                    // this.file = null;
                    this.$refs.fileValue.value = ''
                    this.uploading = false;
                    this.loading = false;
                    return;
                  }
                  this.waitNum = this.waitUpLoad.length; // 记录长度用于百分比展示
                },
                (err) => {
                  this.$message.error("获取文件数据失败");
                  this.file = null;
                  this.$refs.fileValue.value = ''
                  this.uploading = false;
                  this.loading = false;
                  return;
                }
              );
            } else {
              // this.UploadStatus = "文件读取失败";
            }
          },
          (err) => {
            // this.UploadStatus = "文件读取失败";
            this.uploading = false;
            this.loading = false;
            this.$message.error("文件读取失败");
          }
        );
      } else {
        //this.limitFileSize = true;
        this.$message.error("请上传小于100G的文件");
        this.file = null;
        this.$refs.fileValue.value = ''
        this.uploading = false;
      }
    },
    //准备上传
    getFile() {
      /**
       * @Description
       * 代码注释说明
       * 确定按钮
       * @Return
       */
      if (this.file === null) {
        this.$message.error("请先上传文件");
        return;
      }
      this.percentageFlage = this.percentage == 100;
      this.sliceFile(); // 上传切片
    },
    async sliceFile() {
      /**
       * @Description
       * 代码注释说明
       * 如果已上传文件且生成了文件路径
       * @Return
       */
      if (this.fileData.path) {
        return;
      }
      /**
       * @Description
       * 代码注释说明
       * 如果是切片已全部上传 但还未完成合并及移除chunk操作 没有生成文件路径时
       * @Return
       */
      if (this.percentageFlage && !this.fileData.path) {
        this.finishUpload();
        return;
      }
      this.uploading = true;
      this.stoped = false;
      //提交切片
      this.upLoadFileSlice();
    },
    async upLoadFileSlice() {
      if (this.stoped) {
        this.uploading = false;
        return;
      }
      /**
       * @Description
       * 代码注释说明
       * 剩余切片数为0时调用结束上传接口
       * @Return
       */
      try {
        let suffix = /.([0-9A-z]+)$/.exec(this.file.name)[1]; // 文件后缀名也就是文件类型
        let data = {
          bucketName: "static",//桶的名字
          contentType: this.file.type || suffix,//文件类型
          filename: this.file.name,//文件名字
          partCount: this.waitUpLoad.length,//分片多少也就是分了多少个
        };
        //根据分片长度获取分片上传地址以及上传ID和文件地址
        Imageinit(data).then((res) => {
          if (res.data.code == 200 && res.data.data) {
            this.uploadId = res.data.data.uploadId;//文件对应的id
            this.fullPath = res.data.data.fullPath;//上传合并的地址
            this.uploadUrls = res.data.data.uploadUrls;//每个分片对应的位置
            if (this.uploadUrls && this.uploadUrls.length) {
              /**
               * 用于并发上传 parallelRun
               */
              // this.waitUpLoad.forEach((item, i) => {
              //   item.formData.append("Upurl", this.uploadUrls[i]);
              // });
              // this.parallelRun(this.waitUpLoad)
 
              // return;
              let i = 0;//第几个分片对应地址
              /**
               * 文件分片合并
               */
              const complete = () => {
                Imagecomplete({
                  bucketName: "static",//MinIO桶名称
                  fullPath: this.fullPath,//Imageinit返回的上传地址
                  uploadId: this.uploadId,//Imageinit返回的上传id
                }).then(
                  (res) => {
                    if (res.data.data) {
                      this.uploading = false;
                      this.$emit("fileinput", {
                        url: "/*minIo桶地址*/" + this.fullPath,//最终文件路径表单提交用
                        code: this.hash,//md5值校验
                        imageSize: this.imageSize,//文件大小
                        name: this.file.name,//文件名
                      });
                      this.$message({
                        type: "success",
                        message: "上传镜像成功",
                      });
                      this.$refs.fileValue.value = ''
                      this.uploading = false;
                    } else {
                      this.file = null;
                      this.$refs.fileValue.value = ''
                      this.uploading = false;
                      this.$message.error("合并失败");
                    }
                  },
                  (err) => {
                    this.file = null;
                    this.$refs.fileValue.value = ''
                    this.uploading = false;
                    this.$message.error("合并失败");
                  }
                );
              };
              /**
               * 分片上传
               */
              const send = async () => {
                if (!this.show) {
                  this.file = null;
                  this.$refs.fileValue.value = ''
                  this.uploading = false;
                  return;
                }
 
                /**
                 * 没有可上传的请求合并
                 */
                if (i >= this.uploadUrls.length) {
                  // alert('发送完毕')
                  // 发送完毕
                  complete();
                  return;
                }
                if (this.waitNum == 0) return;
 
                /**
                 * 通过AXIOS的put将对应的分片文件传到对应的桶里
                 */
                try {
                  axios
                    .put(
                      this.uploadUrls[i],
                      this.waitUpLoad[i].formData.get("file")
                    )
                    .then(
                      (result) => {
                        /*上传一个分片成功就对应减少一个再接着下一个分片上传
                        */
                        this.waitNum--;
                        i++;
                        send();
                      },
                      (err) => {
                        this.file = null;
                        this.$refs.fileValue.value = ''
                        this.uploading = false;
                        this.$message.error("上传失败");
                      }
                    );
                } catch (error) {
                  this.file = null;
                  this.$refs.fileValue.value = ''
                  this.uploading = false;
                  this.$message.error("上传失败");
                }
              };
              send(); // 发送请求
            }
          }
        });
      } catch (error) {
        this.file = null;
        this.$refs.fileValue.value = ''
        this.uploading = false;
        this.$message.error("上传失败");
      }
    },
    
    inputChange() {
      this.$refs["fileValue"].dispatchEvent(new MouseEvent("click"));
    },  
    /**
     * 用于并发分片上传
     * requestList 上传列表 max几个上传并发执行
     */
    async parallelRun(requestList, max = 10) {
      const requestSliceList = [];
      for (let i = 0; i < requestList.length; i += max) {
        requestSliceList.push(requestList.slice(i, i + max));
      }
 
      for (let i = 0; i  axios.put(
            fn.formData.get("Upurl"),
            fn.formData.get("file")
          )));
          res.forEach(item => {
            this.waitNum--
          })
          console.log('接口返回值为:', res);
          if (this.waitNum === 0) {
            //alert('发送完毕')
 
            // 发送完毕
            this.complete();
            return;
          }
          // const res = await Promise.all(group.map(fn => fn));
 
        } catch (error) {
          console.error(error);
        }
      }
 
    },
    complete() {
      Imagecomplete({
        bucketName: "static",//对应的桶
        fullPath: this.fullPath,//桶的地址
        uploadId: this.uploadId,//桶的id
      }).then(
        (res) => {
          if (res.data.data) {
            this.uploading = false;
            this.$emit("fileinput", {
              url: "/*minIo桶地址*/" + this.fullPath,//'https://asgad/fileinput'+'/1000/20240701/xxx.zip'
              code: this.hash,//文件MD5值
              imageSize: this.imageSize,//文件大小
            });
            this.$message({
              type: "success",
              message: "上传镜像成功",
            });
            this.$refs.fileValue.value = ''
            this.uploading = false;
          } else {
            this.file = null;
            this.$refs.fileValue.value = ''
            this.uploading = false;
            this.$message.error("合并失败");
          }
        },
        (err) => {
          this.file = null;
          this.$refs.fileValue.value = ''
          this.uploading = false;
          this.$message.error("合并失败");
        }
      );
    },
    /**
     * 获取大文件的MD5数值
     * @param {*} file 文件
     * @param {*} n 分片大小单位M
     */
    computeFileMD5(file, n = 50 * 1024 * 1024) {
      //("开始计算...", file);
      return new Promise((resolve, reject) => {
        let blobSlice =
          File.prototype.slice ||
          File.prototype.mozSlice ||
          File.prototype.webkitSlice;
        let chunkSize = n; // 默认按照一片 50MB 分片
        let chunks = Math.ceil(file.size / chunkSize); // 片数
        let currentChunk = 0;
        let spark = new SparkMD5.ArrayBuffer();
        let fileReader = new FileReader();
        let that = this;
        fileReader.onload = function (e) {
          //console.log("read chunk nr", currentChunk + 1, "of", chunks);
          spark.append(e.target.result);
          currentChunk++;
          // console.log("执行进度:" + (currentChunk / chunks) * 100 + "%");
          if (currentChunk = file.size ? file.size : start + chunkSize;
          fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
        }
 
        loadNext();
      });
    },
  },
};

页面样式 

 自行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
.file-choose-btn {
  overflow: hidden;
  position: relative;
 
  input {
    position: absolute;
    font-size: 100px;
    right: 0;
    top: 0;
    opacity: 0;
    cursor: pointer;
  }
}
 
.tips {
  margin-top: 30px;
  font-size: 14px;
  font-weight: 400;
  color: #606266;
}
 
.file-list {
  margin-top: 10px;
}
 
.list-item {
  display: block;
  margin-right: 10px;
  color: #606266;
  line-height: 25px;
  margin-bottom: 5px;
  width: 90%;
 
  .percentage {
    float: right;
  }
}
 
.list-enter-active,
.list-leave-active {
  transition: all 1s;
}
 
.list-enter,
.list-leave-to
 
/* .list-leave-active for below version 2.1.8 */
  {
  opacity: 0;
  transform: translateY(-30px);
}

总结

到此这篇关于前端大文件分片MinIO上传的文章就介绍到这了,更多相关前端大文件分片MinIO上传内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/navsub/js/13074.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部