当前位置:   article > 正文

文件分片上传 springboot+vue3_vue 分片上传文件springboot

vue 分片上传文件springboot

文件分片上传

项目地址

效果

效果

后端

使用了开源组件:x-file-storage gitee地址 组件官网:官网

代码

    @Autowired
    private FileStorageService fileStorageService;


    /**
     * 初始化 分片信息
     * @param fileUploadInfo
     * @return
     */
    @GetMapping(value = "/uploader/chunk")
    public FileInfo init(FileUploadInfo fileUploadInfo) {
        return fileStorageService.initiateMultipartUpload()
                .setSize(fileUploadInfo.getTotalSize())
                .setOriginalFilename(fileUploadInfo.getFilename())
                .init();
    }

    /**
     * 上传 分片文件
     * @param fileUploadInfo
     * @return
     */
    @PostMapping(value = "/uploader/chunk")
    public void bigFile(FileUploadInfo fileUploadInfo) {
        final FileInfo fileInfo = fileStorageService.getFileInfoByUrl(fileUploadInfo.getUrl());

        final MultipartFileWrapper fileWrapper = new MultipartFileWrapper(
                fileUploadInfo.getUpfile(),
                fileUploadInfo.getUpfile().getOriginalFilename(),
                fileUploadInfo.getUpfile().getContentType(),
                fileUploadInfo.getUpfile().getSize());

        fileStorageService.uploadPart(fileInfo, fileUploadInfo.getChunkNumber(),
                        fileWrapper, fileUploadInfo.getCurrentChunkSize().longValue()).upload();
    }

    /**
     * 合并 分片文件
     * @param url
     * @return
     */
    @PostMapping(value = "/uploader/merge")
    public FileInfo merge(@RequestParam String url) {
        final FileInfo fileInfo = fileStorageService.getFileInfoByUrl(url);
        return fileStorageService.completeMultipartUpload(fileInfo).complete();
    }
  • 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
@Data
public class FileUploadInfo {
    /**
     * 操作 文件 的key
    */
    private String url;

    private Integer chunkNumber;
    private Integer chunkSize;
    private Integer currentChunkSize;
    private Long totalSize;
    private String identifier;
    private String filename;
    private String relativePath;
    private Integer totalChunks;

    private MultipartFile upfile;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

配置

docker pull minio/minio

mkdir -p /opt/minio/{data,config}

docker run \
-p 9000:9000 \
-p 9090:9090 \
--name minio \
-e "MINIO_ROOT_USER=minio" \
-e "MINIO_ROOT_PASSWORD=minio123456" \
-v /opt/minio/data:/data \
-v /opt/minio/config:/root/.minio \
minio/minio server /data --console-address ":9090" -address ":9000"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
server:
  port: 8000

spring:
  datasource:
    url: jdbc:h2:file:./data/test_file/file;AUTO_SERVER=TRUE;MODE=MYSQL;DB_CLOSE_DELAY=-1;AUTO_RECONNECT=TRUE
    username: root
    password: 123456
    driver-class-name: org.h2.Driver

  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 201MB

dromara:
  x-file-storage:
    default-platform: minio
    minio:
      - platform: minio
        enable-storage: true
        access-key: zygoJJAKsIZHyXkdKfa4
        secret-key: RBd3Coi6LJnJPEhv6IEUEEnhm0Mums4VMi1bS6SK
        end-point: http://192.168.64.129:9000
        bucket-name: test
        domain:
        base-path:
  • 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

前端

参考博客1:在vue3项目中实现文件分片上传vue-simple-uploader_vue3分片上传插件-CSDN博客

参考博客2:基于vue-simple-uploader封装文件分片上传组件-CSDN博客

代码

<template>
  <div>
    <!-- 上传器 -->
    <uploader
        ref="uploaderRef"
        :options="options"
        :autoStart="false"
        :file-status-text="fileStatusText"
        class="uploader-ui"
        @file-added="onFileAdded"
        @file-success="onFileSuccess"
        @file-progress="onFileProgress"
        @file-error="onFileError"
    >
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <div>
          <uploader-btn id="global-uploader-btn" ref="uploadBtn" :attrs="attrs">
            选择文件
            <el-icon><Upload /></el-icon>
          </uploader-btn>
        </div>
      </uploader-drop>
      <uploader-list></uploader-list>
    </uploader>
  </div>
</template>

<script setup>
import { ACCEPT_CONFIG } from '@/config/accept.ts';
import { reactive, ref } from 'vue';
import SparkMD5 from 'spark-md5';
import { mergeFile } from '@/api/fileUpload/index';
import { ElMessage } from 'element-plus';

const options = reactive({
  //目标上传 URL,默认POST, import.meta.env.VITE_API_URL = api
  // target ==》http://localhost:6666/api/uploader/chunk
  target: import.meta.env.VITE_API_URL + '/uploader/chunk',
  query: {},
  headers: {
    // 需要携带token信息,当然看各项目情况具体定义
    token: "your_token",
  },
  //分块大小(单位:字节)
  chunkSize: '5242880',
  url: '',
  //上传文件时文件内容的参数名,对应chunk里的Multipart对象名,默认对象名为file
  fileParameterName: 'upfile',
  //失败后最多自动重试上传次数
  maxChunkRetries: 3,
  //是否开启服务器分片校验,对应GET类型同名的target URL
  testChunks: true,
  // 服务器分片校验函数
  checkChunkUploadedByResponse: function (chunk, response_msg) {
    let objMessage = JSON.parse(response_msg);

    if (!this.url) {
      options.url = objMessage?.url;
    }

    if (objMessage?.attr?.skipUpload) {
      return true;
    }

    return (objMessage.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
  },
  processParams: function(params, file) {
    if (!params.url) {
      params.url = options.url
    }
    return params
  }
});
const attrs = reactive({
  accept: ACCEPT_CONFIG.getAll(),
});
const fileStatusText = reactive({
  success: '上传成功',
  error: '上传失败',
  uploading: '上传中',
  paused: '暂停',
  waiting: '等待上传',
});
onMounted(() => {
});
function onFileAdded(file) {
  computeMD5(file);
}

function onFileSuccess(rootFile, file, response, chunk) {
  //refProjectId为预留字段,可关联附件所属目标,例如所属档案,所属工程等
  file.refProjectId = '';
  mergeFile(options.url)
      .then((responseData) => {
        if (responseData.data.code === 415) {
          console.log('合并操作未成功,结果码:' + responseData.data.code);
        }
        ElMessage.success(responseData.data);
        options.url = '';
      })
      .catch(function (error) {
        console.log('合并后捕获的未知异常:' + error);
      })
}

function onFileError(rootFile, file, response, chunk) {
  console.log('上传完成后异常信息:' + response);
}

function onFileProgress(rootFile, file, chunk) {
  // 文件进度的回调
  // console.log('on-file-progress', rootFile, file, chunk)
}

/**
 * 计算md5,实现断点续传及秒传
 * @param file
 */
function computeMD5(file) {
  file.pause();

  //单个文件的大小限制2G
  let fileSizeLimit = 2 * 1024 * 1024 * 1024;
  console.log('文件大小:' + file.size);
  console.log('限制大小:' + fileSizeLimit);
  if (file.size > fileSizeLimit) {
    file.cancel();
  }

  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 = 10 * 1024 * 1000;
  let chunks = Math.ceil(file.size / chunkSize);
  let spark = new SparkMD5.ArrayBuffer();
  //由于计算整个文件的Md5太慢,因此采用只计算第1块文件的md5的方式
  let chunkNumberMD5 = 1;

  loadNext();

  fileReader.onload = (e) => {
    spark.append(e.target.result);

    if (currentChunk < chunkNumberMD5) {
      loadNext();
    } else {
      let md5 = spark.end();
      file.uniqueIdentifier = md5;
      file.resume();
      console.log(
          `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
              file.size
          } 用时:${new Date().getTime() - time} ms`
      );
    }
  };

  fileReader.onerror = function () {
    error(`文件${file.name}读取出错,请检查该文件`);
    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));
    currentChunk++;
    console.log('计算第' + currentChunk + '块');
  }
}
const uploaderRef = ref();
function close() {
  uploaderRef.value.cancel();
}
function error(msg) {
  console.log(msg, 'msg');
}
</script>

<style scoped>
.uploader-ui {
  padding: 15px;
  margin: 40px auto 0;
  font-size: 12px;
  font-family: Microsoft YaHei;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
}
.uploader-ui .uploader-btn {
  margin-right: 4px;
  font-size: 12px;
  border-radius: 3px;
  color: #fff;
  background-color: #409eff;
  border-color: #409eff;
  display: inline-block;
  line-height: 1;
  white-space: nowrap;
}
.uploader-ui .uploader-list {
  max-height: 440px;
  overflow: auto;
  overflow-x: hidden;
  overflow-y: auto;
}
</style>

  • 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

与上面的参考代码差不多,不过在兼容后端有一些调整。以下为差异点

差异1
差异2

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号