利用SpringBoot和vue-simple-uploader進行文件的分片上傳

利用SpringBoot和vue-simple-uploader進行文件的分片上傳

效果【上傳Zip文件爲例,可以自行擴展】

引入vue-simple-uploader

  1. 安裝上傳插件
    npm install vue-simple-uploader --save
    
  2. main.js全局引入上傳插件
    import uploader from 'vue-simple-uploader'
    Vue.use(uploader)
    
  3. 安裝md5校驗插件(保證上傳文件的完整性和一致性)
    npm install spark-md5 --save
    

頁面

<template>
  <div>
    <uploader :key="uploader_key" :options="options" class="uploader-example"
              :autoStart="false"
              @file-success="onFileSuccess"
              @file-added="filesAdded">
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <uploader-btn :single="true" :attrs="attrs">選擇Zip文件</uploader-btn>
      </uploader-drop>
      <uploader-list></uploader-list>
    </uploader>
  </div>
</template>

<script>
  import SparkMD5 from 'spark-md5';

  export default {
    data() {
      return {
        uploader_key: new Date().getTime(),
        options: {
          target: '/chunk/chunkUpload',
          testChunks: false,
        },
        attrs: {
          accept: '.zip'
        }
      }
    },
    methods: {
      onFileSuccess: function (rootFile, file, response, chunk) {
        console.log(JSON.parse(response).model);
      },
      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 = 10 * 1024 * 1000;
        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();
            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();//開始上傳
      },
      filesAdded(file, event) {
        //大小判斷
        const isLt100M = file.size / 1024 / 1024 < 10;
        if (!isLt100M) {
          this.$message.error(this.$t("error.error_upload_file_max"));
        } else {
          this.computeMD5(file)
        }
      }
    }
  }
</script>

<style>
  .uploader-example {
    width: 90%;
    padding: 15px;
    margin: 40px auto 0;
    font-size: 12px;
    box-shadow: 0 0 10px rgba(0, 0, 0, .4);
  }

  .uploader-example .uploader-btn {
    margin-right: 4px;
  }

  .uploader-example .uploader-list {
    max-height: 440px;
    overflow: auto;
    overflow-x: hidden;
    overflow-y: auto;
  }
</style>

後臺

  1. 引入工具

    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.6</version>
    </dependency>
    
    <dependency>
        <groupId>commons-lang</groupId>
        <artifactId>commons-lang</artifactId>
        <version>2.6</version>
    </dependency>
    
  2. 控制類

    import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.File;
    
    @RestController
    @RequestMapping("/chunk")
    public class ChunkController {
        @RequestMapping("/chunkUpload")
        public StdOut chunkUpload(MultipartFileParam param, HttpServletRequest request, HttpServletResponse response) {
            StdOut out = new StdOut();
    
            File file = new File("C:\\chunk_test");//存儲路徑
    
            ChunkService chunkService = new ChunkService();
    
            String path = file.getAbsolutePath();
            response.setContentType("text/html;charset=UTF-8");
    
            try {
                //判斷前端Form表單格式是否支持文件上傳
                boolean isMultipart = ServletFileUpload.isMultipartContent(request);
                if (!isMultipart) {
                    out.setCode(StdOut.PARAMETER_NULL);
                    out.setMessage("表單格式錯誤");
                    return out;
                } else {
                    param.setTaskId(param.getIdentifier());
                    out.setModel(chunkService.chunkUploadByMappedByteBuffer(param, path));
                    return out;
                }
            } catch (NotSameFileExpection e) {
                out.setCode(StdOut.FAIL);
                out.setMessage("MD5校驗失敗");
                return out;
            } catch (Exception e) {
                out.setCode(StdOut.FAIL);
                out.setMessage("上傳失敗");
                return out;
            }
        }
    }
    
  3. StdOut類(只是封裝的返回類)

    public class StdOut {
        public static final int SUCCESS = 200;
        public static final int FAIL = 400;
        public static final int PARAMETER_NULL = 500;
        public static final int NO_LOGIN = 600;
        private int code = 200;
        private Object model = null;
        private String message = null;
    
        public StdOut() {
            this.setCode(200);
            this.setModel((Object)null);
        }
    
        public StdOut(int code) {
            this.setCode(code);
            this.setModel((Object)null);
        }
    
        public StdOut(List<Map<String, Object>> model) {
            this.setCode(200);
            this.setModel(model);
        }
    
        public StdOut(int code, List<Map<String, Object>> model) {
            this.setCode(code);
            this.setModel(model);
        }
    
        public int getCode() {
            return this.code;
        }
    
        public void setCode(int code) {
            this.code = code;
        }
    
        public String toString() {
            return JSON.toJSONString(this);
        }
    
        public Object getModel() {
            return this.model;
        }
    
        public void setModel(Object model) {
            this.model = model;
        }
    
        public String getMessage() {
            return this.message;
        }
    
        public void setMessage(String message) {
            this.message = message;
        }
    }
    
  4. MultipartFileParam類(文件信息類)

    import org.springframework.web.multipart.MultipartFile;
    
    public class MultipartFileParam {
        private String taskId;
        private int chunkNumber;
        private long chunkSize;
        private int totalChunks;
        private String identifier;
        private MultipartFile file;
    
        public String getTaskId() {
            return taskId;
        }
    
        public void setTaskId(String taskId) {
            this.taskId = taskId;
        }
    
        public int getChunkNumber() {
            return chunkNumber;
        }
    
        public void setChunkNumber(int chunkNumber) {
            this.chunkNumber = chunkNumber;
        }
    
        public long getChunkSize() {
            return chunkSize;
        }
    
        public void setChunkSize(long chunkSize) {
            this.chunkSize = chunkSize;
        }
    
        public int getTotalChunks() {
            return totalChunks;
        }
    
        public void setTotalChunks(int totalChunks) {
            this.totalChunks = totalChunks;
        }
    
        public String getIdentifier() {
            return identifier;
        }
    
        public void setIdentifier(String identifier) {
            this.identifier = identifier;
        }
    
        public MultipartFile getFile() {
            return file;
        }
    
        public void setFile(MultipartFile file) {
            this.file = file;
        }
    }
    
  5. ChunkService類

    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.commons.io.FileUtils;
    import org.apache.commons.lang.StringUtils;
    
    import java.io.File;
    import java.io.FileInputStream;
    import java.io.IOException;
    import java.io.RandomAccessFile;
    import java.nio.ByteBuffer;
    import java.nio.channels.FileChannel;
    import java.util.UUID;
    
    public class ChunkService {
        public String chunkUploadByMappedByteBuffer(MultipartFileParam param, String filePath) throws IOException, NotSameFileExpection {
    
            if (param.getTaskId() == null || "".equals(param.getTaskId())) {
                param.setTaskId(UUID.randomUUID().toString());
            }
    
            String fileName = param.getFile().getOriginalFilename();
            String tempFileName = param.getTaskId() + fileName.substring(fileName.lastIndexOf(".")) + "_tmp";
            File fileDir = new File(filePath);
            if (!fileDir.exists()) {
                fileDir.mkdirs();
            }
            File tempFile = new File(filePath, tempFileName);
            //第一步 打開將要寫入的文件
            RandomAccessFile raf = new RandomAccessFile(tempFile, "rw");
            //第二步 打開通道
            FileChannel fileChannel = raf.getChannel();
            //第三步 計算偏移量
            long position = (param.getChunkNumber() - 1) * param.getChunkSize();
            //第四步 獲取分片數據
            byte[] fileData = param.getFile().getBytes();
            //第五步 寫入數據
            fileChannel.position(position);
            fileChannel.write(ByteBuffer.wrap(fileData));
            fileChannel.force(true);
            fileChannel.close();
            raf.close();
            //判斷是否完成文件的傳輸並進行校驗與重命名
            boolean isComplete = checkUploadStatus(param, fileName, filePath);
            if (isComplete) {
                FileInputStream fileInputStream = new FileInputStream(tempFile.getPath());
                String md5 = DigestUtils.md5Hex(fileInputStream);
                fileInputStream.close();
                if (StringUtils.isNotBlank(md5) && !md5.equals(param.getIdentifier())) {
                    throw new NotSameFileExpection();
                }
                renameFile(tempFile, fileName);
                return fileName;
            }
            return null;
        }
    
        public void renameFile(File toBeRenamed, String toFileNewName) {
            if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
                System.err.println("文件不存在");
                return;
            }
            String p = toBeRenamed.getParent();
            File newFile = new File(p + File.separatorChar + toFileNewName);
            toBeRenamed.renameTo(newFile);
        }
    
        public boolean checkUploadStatus(MultipartFileParam param, String fileName, String filePath) throws IOException {
            File confFile = new File(filePath, fileName + ".conf");
            RandomAccessFile confAccessFile = new RandomAccessFile(confFile, "rw");
            //設置文件長度
            confAccessFile.setLength(param.getTotalChunks());
            //設置起始偏移量
            confAccessFile.seek(param.getChunkNumber() - 1);
            //將指定的一個字節寫入文件中 127,
            confAccessFile.write(Byte.MAX_VALUE);
            byte[] completeStatusList = FileUtils.readFileToByteArray(confFile);
            confAccessFile.close();//不關閉會造成無法佔用
            //創建conf文件文件長度爲總分片數,每上傳一個分塊即向conf文件中寫入一個127,那麼沒上傳的位置就是默認的0,已上傳的就是127
            for (int i = 0; i < completeStatusList.length; i++) {
                if (completeStatusList[i] != Byte.MAX_VALUE) {
                    return false;
                }
            }
            confFile.delete();
            return true;
        }
    }
    

  6.NotSameFileExpection類

public class NotSameFileExpection extends Exception {
    public NotSameFileExpection() {
        super("File MD5 Different");
    }
}

遇到問題

  • 根據自己的實際情況進行取捨,靈活處理
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章