springboot整合vue2-uploader文件分片上传、秒传、断点续传

news2024/10/5 20:25:00

1:vue-simple-uploader介绍

vue-simple-uploader是基于 simple-uploader.js 封装的vue上传插件。它的优点包括且不限于以下几种:

  • 支持文件、多文件、文件夹上传;支持拖拽文件、文件夹上传
  • 可暂停、继续上传
  • 错误处理
  • 支持“秒传”,通过文件判断服务端是否已存在从而实现“秒传”
  • 分片上传
  • 支持进度、预估剩余时间、出错自动重试、重传等操作

2:图片便于理解:

秒传:(将文件使用MD5加密,生成一个串,我们拿到这个串到redis 中查看是否存在)

3:服务端Java代码

3.1  UploaderController

package com.xialj.demoend.controller;

import com.xialj.demoend.common.RestApiResponse;
import com.xialj.demoend.common.Result;
import com.xialj.demoend.dto.FileChunkDTO;
import com.xialj.demoend.dto.FileChunkResultDTO;
import com.xialj.demoend.service.IUploadService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * @ProjectName UploaderController
 * @author Administrator
 * @version 1.0.0
 * @Description 附件分片上传
 * @createTime 2022/4/13 0013 15:58
 */
@RestController
@RequestMapping("upload")
public class UploaderController {

   @Resource
   private IUploadService uploadService;

   /**
    * 检查分片是否存在
    *
    * @return
    */
   @GetMapping("chunk")
   public Result checkChunkExist(FileChunkDTO chunkDTO) {
      FileChunkResultDTO fileChunkCheckDTO;
      try {
         fileChunkCheckDTO = uploadService.checkChunkExist(chunkDTO);
         return Result.ok(fileChunkCheckDTO);
      } catch (Exception e) {
         return Result.fail(e.getMessage());
      }
   }


   /**
    * 上传文件分片
    *
    * @param chunkDTO
    * @return
    */
   @PostMapping("chunk")
   public Result uploadChunk(FileChunkDTO chunkDTO) {
      try {
         uploadService.uploadChunk(chunkDTO);
         return Result.ok(chunkDTO.getIdentifier());
      } catch (Exception e) {
         return Result.fail(e.getMessage());
      }
   }

   /**
    * 请求合并文件分片
    *
    * @param chunkDTO
    * @return
    */
   @PostMapping("merge")
   public Result mergeChunks(@RequestBody FileChunkDTO chunkDTO) {
      try {
         boolean success = uploadService.mergeChunk(chunkDTO.getIdentifier(), chunkDTO.getFilename(), chunkDTO.getTotalChunks());
         return Result.ok(success);
      } catch (Exception e) {
         return Result.fail(e.getMessage());
      }
   }

}

3.2  IUploadService  接口

package com.xialj.demoend.service;
import com.xialj.demoend.dto.FileChunkDTO;
import com.xialj.demoend.dto.FileChunkResultDTO;

import java.io.IOException;

/**
 * @ProjectName IUploadService
 * @author Administrator
 * @version 1.0.0
 * @Description 附件分片上传
 * @createTime 2022/4/13 0013 15:59
 */
public interface IUploadService {

   /**
    * 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合
    * @param chunkDTO
    * @return
    */
   FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO);


   /**
    * 上传文件分片
    * @param chunkDTO
    */
   void uploadChunk(FileChunkDTO chunkDTO) throws IOException;


   /**
    * 合并文件分片
    * @param identifier
    * @param fileName
    * @param totalChunks
    * @return
    * @throws IOException
    */
   boolean mergeChunk(String identifier,String fileName,Integer totalChunks)throws IOException;
}

3.3 UploadServiceImpl

package com.xialj.demoend.service.impl;
import com.xialj.demoend.dto.FileChunkDTO;
import com.xialj.demoend.dto.FileChunkResultDTO;
import com.xialj.demoend.service.IUploadService;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.io.*;
import java.util.*;


/**
 * @ProjectName UploadServiceImpl
 * @author Administrator
 * @version 1.0.0
 * @Description 附件分片上传
 * @createTime 2022/4/13 0013 15:59
 */
@Service
@SuppressWarnings("all")
public class UploadServiceImpl implements IUploadService {

   private Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);

   @Autowired
   private RedisTemplate<String, Object> redisTemplate;

   @Value("${uploadFolder}")
   private String uploadFolder;

   /**
    * 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合
    *  检查分片是否存在
          ○ 检查目录下的文件是否存在。
          ○ 检查redis存储的分片是否存在。
          ○ 判断分片数量和总分片数量是否一致。
               如果文件存在并且分片上传完毕,标识已经完成附件的上传,可以进行秒传操作。
               如果文件不存在或者分片为上传完毕,则返回false并返回已经上传的分片信息。
    * @param chunkDTO
    * @return
    */
   @Override
   public FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) {
      //1.检查文件是否已上传过
      //1.1)检查在磁盘中是否存在
      String fileFolderPath = getFileFolderPath(chunkDTO.getIdentifier());
      logger.info("fileFolderPath-->{}", fileFolderPath);
      String filePath = getFilePath(chunkDTO.getIdentifier(), chunkDTO.getFilename());
      File file = new File(filePath);
      boolean exists = file.exists();
      //1.2)检查Redis中是否存在,并且所有分片已经上传完成。
      Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");
      if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks() && exists) {
         return new FileChunkResultDTO(true);
      }
      File fileFolder = new File(fileFolderPath);
      if (!fileFolder.exists()) {
         boolean mkdirs = fileFolder.mkdirs();
         logger.info("准备工作,创建文件夹,fileFolderPath:{},mkdirs:{}", fileFolderPath, mkdirs);
      }
      // 断点续传,返回已上传的分片
      return new FileChunkResultDTO(false, uploaded);
   }


   /**
    * 上传分片
    *  上传附件分片
           ○ 判断目录是否存在,如果不存在则创建目录。
           ○ 进行切片的拷贝,将切片拷贝到指定的目录。
           ○ 将该分片写入redis
    * @param chunkDTO
    */
   @Override
   public void uploadChunk(FileChunkDTO chunkDTO) {
      //分块的目录
      String chunkFileFolderPath = getChunkFileFolderPath(chunkDTO.getIdentifier());
      logger.info("分块的目录 -> {}", chunkFileFolderPath);
      File chunkFileFolder = new File(chunkFileFolderPath);
      if (!chunkFileFolder.exists()) {
         boolean mkdirs = chunkFileFolder.mkdirs();
         logger.info("创建分片文件夹:{}", mkdirs);
      }
      //写入分片
      try (
              InputStream inputStream = chunkDTO.getFile().getInputStream();
              FileOutputStream outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunkDTO.getChunkNumber()))
      ) {
         IOUtils.copy(inputStream, outputStream);
         logger.info("文件标识:{},chunkNumber:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber());
         //将该分片写入redis
         long size = saveToRedis(chunkDTO);
      } catch (Exception e) {
         e.printStackTrace();
      }
   }


   @Override
   public boolean mergeChunk(String identifier, String fileName, Integer totalChunks) throws IOException {
      return mergeChunks(identifier, fileName, totalChunks);
   }

   /**
    * 合并分片
    *
    * @param identifier
    * @param filename
    */
   private boolean mergeChunks(String identifier, String filename, Integer totalChunks) {
      String chunkFileFolderPath = getChunkFileFolderPath(identifier);
      String filePath = getFilePath(identifier, filename);
      // 检查分片是否都存在
      if (checkChunks(chunkFileFolderPath, totalChunks)) {
         File chunkFileFolder = new File(chunkFileFolderPath);
         File mergeFile = new File(filePath);
         File[] chunks = chunkFileFolder.listFiles();
         // 切片排序1、2/3、---
         List fileList = Arrays.asList(chunks);
         Collections.sort(fileList, (Comparator<File>) (o1, o2) -> {
            return Integer.parseInt(o1.getName()) - (Integer.parseInt(o2.getName()));
         });
         try {
            RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
            byte[] bytes = new byte[1024];
            for (File chunk : chunks) {
               RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
               int len;
               while ((len = randomAccessFileReader.read(bytes)) != -1) {
                  randomAccessFileWriter.write(bytes, 0, len);
               }
               randomAccessFileReader.close();
            }
            randomAccessFileWriter.close();
         } catch (Exception e) {
            return false;
         }
         return true;
      }
      return false;
   }

   /**
    * 检查分片是否都存在
    * @param chunkFileFolderPath
    * @param totalChunks
    * @return
    */
   private boolean checkChunks(String chunkFileFolderPath, Integer totalChunks) {
      try {
         for (int i = 1; i <= totalChunks + 1; i++) {
            File file = new File(chunkFileFolderPath + File.separator + i);
            if (file.exists()) {
               continue;
            } else {
               return false;
            }
         }
      } catch (Exception e) {
         return false;
      }
      return true;
   }

   /**
    * 分片写入Redis
    * 判断切片是否已存在,如果未存在,则创建基础信息,并保存。
    * @param chunkDTO
    */
   private synchronized long saveToRedis(FileChunkDTO chunkDTO) {
      Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(chunkDTO.getIdentifier(), "uploaded");
      if (uploaded == null) {
         uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber()));
         HashMap<String, Object> objectObjectHashMap = new HashMap<>();
         objectObjectHashMap.put("uploaded", uploaded);
         objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks());
         objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize());
//            objectObjectHashMap.put("path", getFileRelativelyPath(chunkDTO.getIdentifier(), chunkDTO.getFilename()));
         objectObjectHashMap.put("path", chunkDTO.getFilename());
         redisTemplate.opsForHash().putAll(chunkDTO.getIdentifier(), objectObjectHashMap);
      } else {
         uploaded.add(chunkDTO.getChunkNumber());
         redisTemplate.opsForHash().put(chunkDTO.getIdentifier(), "uploaded", uploaded);
      }
      return uploaded.size();
   }

   /**
    * 得到文件的绝对路径
    *
    * @param identifier
    * @param filename
    * @return
    */
   private String getFilePath(String identifier, String filename) {
      String ext = filename.substring(filename.lastIndexOf("."));
//        return getFileFolderPath(identifier) + identifier + ext;
      return uploadFolder + filename;
   }

   /**
    * 得到文件的相对路径
    *
    * @param identifier
    * @param filename
    * @return
    */
   private String getFileRelativelyPath(String identifier, String filename) {
      String ext = filename.substring(filename.lastIndexOf("."));
      return "/" + identifier.substring(0, 1) + "/" +
              identifier.substring(1, 2) + "/" +
              identifier + "/" + identifier
              + ext;
   }


   /**
    * 得到分块文件所属的目录
    *
    * @param identifier
    * @return
    */
   private String getChunkFileFolderPath(String identifier) {
      return getFileFolderPath(identifier) + "chunks" + File.separator;
   }

   /**
    * 得到文件所属的目录
    *
    * @param identifier
    * @return
    */
   private String getFileFolderPath(String identifier) {
      return uploadFolder + identifier.substring(0, 1) + File.separator +
              identifier.substring(1, 2) + File.separator +
              identifier + File.separator;
//        return uploadFolder;
   }
}

3.4  FileChunkDTO

package com.xialj.demoend.dto;
import org.springframework.web.multipart.MultipartFile;

/**
 * @ProjectName FileChunkDTO
 * @author Administrator
 * @version 1.0.0
 * @Description 附件分片上传
 * @createTime 2022/4/13 0013 15:59
 */
public class FileChunkDTO {
   /**
    * 文件 md5
    */
   private String identifier;
   /**
    * 分块文件
    */
   MultipartFile file;
   /**
    * 当前分块序号
    */
   private Integer chunkNumber;
   /**
    * 分块大小
    */
   private Long chunkSize;
   /**
    * 当前分块大小
    */
   private Long currentChunkSize;
   /**
    * 文件总大小
    */
   private Long totalSize;
   /**
    * 分块总数
    */
   private Integer totalChunks;
   /**
    * 文件名
    */
   private String filename;

   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;
   }


   public Integer getChunkNumber() {
      return chunkNumber;
   }

   public void setChunkNumber(Integer chunkNumber) {
      this.chunkNumber = chunkNumber;
   }

   public Long getChunkSize() {
      return chunkSize;
   }

   public void setChunkSize(Long chunkSize) {
      this.chunkSize = chunkSize;
   }

   public Long getCurrentChunkSize() {
      return currentChunkSize;
   }

   public void setCurrentChunkSize(Long currentChunkSize) {
      this.currentChunkSize = currentChunkSize;
   }

   public Long getTotalSize() {
      return totalSize;
   }

   public void setTotalSize(Long totalSize) {
      this.totalSize = totalSize;
   }

   public Integer getTotalChunks() {
      return totalChunks;
   }

   public void setTotalChunks(Integer totalChunks) {
      this.totalChunks = totalChunks;
   }

   public String getFilename() {
      return filename;
   }

   public void setFilename(String filename) {
      this.filename = filename;
   }

   @Override
   public String toString() {
      return "FileChunkDTO{" +
              "identifier='" + identifier + '\'' +
              ", file=" + file +
              ", chunkNumber=" + chunkNumber +
              ", chunkSize=" + chunkSize +
              ", currentChunkSize=" + currentChunkSize +
              ", totalSize=" + totalSize +
              ", totalChunks=" + totalChunks +
              ", filename='" + filename + '\'' +
              '}';
   }
}

3.5 FileChunkResultDTO

package com.xialj.demoend.dto;


import java.util.Set;

/**
 * @ProjectName FileChunkResultDTO
 * @author Administrator
 * @version 1.0.0
 * @Description 附件分片上传
 * @createTime 2022/4/13 0013 15:59
 */
public class FileChunkResultDTO {
   /**
    * 是否跳过上传
    */
   private Boolean skipUpload;

   /**
    * 已上传分片的集合
    */
   private Set<Integer> uploaded;

   public Boolean getSkipUpload() {
      return skipUpload;
   }

   public void setSkipUpload(Boolean skipUpload) {
      this.skipUpload = skipUpload;
   }

   public Set<Integer> getUploaded() {
      return uploaded;
   }

   public void setUploaded(Set<Integer> uploaded) {
      this.uploaded = uploaded;
   }


   public FileChunkResultDTO(Boolean skipUpload, Set<Integer> uploaded) {
      this.skipUpload = skipUpload;
      this.uploaded = uploaded;
   }

   public FileChunkResultDTO(Boolean skipUpload) {
      this.skipUpload = skipUpload;
   }
}

3.6 Result

package com.xialj.demoend.common;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
 * @Author 
 * @Date Created in  2023/2/23 17:25
 * @DESCRIPTION:  全局统一返回结果
 * @Version V1.0
 */
@Data
@ApiModel(value = "全局统一返回结果")
@SuppressWarnings("all")
public class Result<T> {

    @ApiModelProperty(value = "返回码")
    private Integer code;

    @ApiModelProperty(value = "返回消息")
    private String message;

    @ApiModelProperty(value = "返回数据")
    private T data;
    private Long total;

    public Result(){}

    protected static <T> Result<T> build(T data) {
        Result<T> result = new Result<T>();
        if (data != null)
            result.setData(data);
        return result;
    }

    public static <T> Result<T> build(T body, ResultCodeEnum resultCodeEnum) {
        Result<T> result = build(body);
        result.setCode(resultCodeEnum.getCode());
        result.setMessage(resultCodeEnum.getMessage());
        return result;
    }

    public static <T> Result<T> build(Integer code, String message) {
        Result<T> result = build(null);
        result.setCode(code);
        result.setMessage(message);
        return result;
    }

    public static<T> Result<T> ok(){
        return Result.ok(null);
    }

    /**
     * 操作成功
     * @param data
     * @param <T>
     * @return
     */
    public static<T> Result<T> ok(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.SUCCESS);
    }

    public static<T> Result<T> fail(){
        return Result.fail(null);
    }

    /**
     * 操作失败
     * @param data
     * @param <T>
     * @return
     */
    public static<T> Result<T> fail(T data){
        Result<T> result = build(data);
        return build(data, ResultCodeEnum.FAIL);
    }

    public Result<T> message(String msg){
        this.setMessage(msg);
        return this;
    }

    public Result<T> code(Integer code){
        this.setCode(code);
        return this;
    }

    public boolean isOk() {
        if(this.getCode().intValue() == ResultCodeEnum.SUCCESS.getCode().intValue()) {
            return true;
        }
        return false;
    }
}

3.6 ResultCodeEnum

package com.xialj.demoend.common;

import lombok.Getter;

/**
   * @Author 
   * @Date Created in  2023/2/23 17:25
   * @DESCRIPTION:  统一返回结果状态信息类
   * @Version V1.0
   */
@Getter
@SuppressWarnings("all")
public enum ResultCodeEnum {

    SUCCESS(200,"成功"),
    FAIL(201, "失败"),
    PARAM_ERROR( 202, "参数不正确"),
    SERVICE_ERROR(203, "服务异常"),
    DATA_ERROR(204, "数据异常"),
    DATA_UPDATE_ERROR(205, "数据版本异常"),

    LOGIN_AUTH(208, "未登陆"),
    PERMISSION(209, "没有权限"),

    CODE_ERROR(210, "验证码错误"),
//    LOGIN_MOBLE_ERROR(211, "账号不正确"),
    LOGIN_DISABLED_ERROR(212, "改用户已被禁用"),
    REGISTER_MOBLE_ERROR(213, "手机号码格式不正确"),
    REGISTER_MOBLE_ERROR_NULL(214, "手机号码为空"),

    LOGIN_AURH(214, "需要登录"),
    LOGIN_ACL(215, "没有权限"),

    URL_ENCODE_ERROR( 216, "URL编码失败"),
    ILLEGAL_CALLBACK_REQUEST_ERROR( 217, "非法回调请求"),
    FETCH_ACCESSTOKEN_FAILD( 218, "获取accessToken失败"),
    FETCH_USERINFO_ERROR( 219, "获取用户信息失败"),
    //LOGIN_ERROR( 23005, "登录失败"),

    PAY_RUN(220, "支付中"),
    CANCEL_ORDER_FAIL(225, "取消订单失败"),
    CANCEL_ORDER_NO(225, "不能取消预约"),

    HOSCODE_EXIST(230, "医院编号已经存在"),
    NUMBER_NO(240, "可预约号不足"),
    TIME_NO(250, "当前时间不可以预约"),

    SIGN_ERROR(300, "签名错误"),
    HOSPITAL_OPEN(310, "医院未开通,暂时不能访问"),
    HOSPITAL_LOCK(320, "医院被锁定,暂时不能访问"),
    HOSPITAL_LOCKKEY(330,"医院对应key不一致")
    ;

    private Integer code;
    private String message;

    private ResultCodeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

4:完成vue2前端的创建

4.1 安装uploaderspark-md5的依赖

npm install --save vue-simple-uploader
npm install --save spark-md5

 

 4.2 mainjs导入uploader

import uploader from 'vue-simple-uploader'
Vue.use(uploader)

 4.3 创建uploader组件

<template>
  <div>
    <uploader
        :autoStart="false"
        :options="options"
        :file-status-text="statusText"
        class="uploader-example"
        @file-complete="fileComplete"
        @complete="complete"
        @file-success="fileSuccess"
        @files-added="filesAdded"
    >
      <uploader-unsupport></uploader-unsupport>
      <uploader-drop>
        <p>将文件拖放到此处以上传</p>
        <uploader-btn>选择文件</uploader-btn>
        <uploader-btn :attrs="attrs">选择图片</uploader-btn>
        <uploader-btn :directory="true">选择文件夹</uploader-btn>
      </uploader-drop>
      <!-- <uploader-list></uploader-list> -->
      <uploader-files> </uploader-files>
    </uploader>
    <br />
    <el-button @click="allStart()" :disabled="disabled">全部开始</el-button>
    <el-button @click="allStop()" style="margin-left: 4px">全部暂停</el-button>
    <el-button @click="allRemove()" style="margin-left: 4px">全部移除</el-button>
  </div>
</template>

<script>
import axios from "axios";
import SparkMD5 from "spark-md5";
import {upload} from "@/api/user";
// import storage from "store";
// import { ACCESS_TOKEN } from '@/store/mutation-types'
export default {
  name: "Home",
  data() {
    return {
      skip: false,
      options: {
        target: "//localhost:9999/upload/chunk",
        // 开启服务端分片校验功能
        testChunks: true,
        parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
          return parsedTimeRemaining
              .replace(/\syears?/, "年")
              .replace(/\days?/, "天")
              .replace(/\shours?/, "小时")
              .replace(/\sminutes?/, "分钟")
              .replace(/\sseconds?/, "秒");
        },
        // 服务器分片校验函数
        checkChunkUploadedByResponse: (chunk, message) => {
          const result = JSON.parse(message);
          if (result.data.skipUpload) {
            this.skip = true;
            return true;
          }
          return (result.data.uploaded || []).indexOf(chunk.offset + 1) >= 0;
        },
        // headers: {
        //   // 在header中添加的验证,请根据实际业务来
        //   "Access-Token": storage.get(ACCESS_TOKEN),
        // },
      },
      attrs: {
        accept: "image/*",
      },
      statusText: {
        success: "上传成功",
        error: "上传出错了",
        uploading: "上传中...",
        paused: "暂停中...",
        waiting: "等待中...",
        cmd5: "计算文件MD5中...",
      },
      fileList: [],
      disabled: true,
    };
  },
  watch: {
    fileList(o, n) {
      this.disabled = false;
    },
  },
  methods: {
    // fileSuccess(rootFile, file, response, chunk) {
    //   // console.log(rootFile);
    //   // console.log(file);
    //   // console.log(message);
    //   // console.log(chunk);
    //   const result = JSON.parse(response);
    //   console.log(result.success, this.skip);
    //
    //   if (result.success && !this.skip) {
    //     axios
    //         .post(
    //             "http://127.0.0.1:9999/upload/merge",
    //             {
    //               identifier: file.uniqueIdentifier,
    //               filename: file.name,
    //               totalChunks: chunk.offset,
    //             },
    //             // {
    //             //   headers: { "Access-Token": storage.get(ACCESS_TOKEN) }
    //             // }
    //         )
    //         .then((res) => {
    //           if (res.data.success) {
    //             console.log("上传成功");
    //           } else {
    //             console.log(res);
    //           }
    //         })
    //         .catch(function (error) {
    //           console.log(error);
    //         });
    //   } else {
    //     console.log("上传成功,不需要合并");
    //   }
    //   if (this.skip) {
    //     this.skip = false;
    //   }
    // },
    fileSuccess(rootFile, file, response, chunk) {
      // console.log(rootFile);
      // console.log(file);
      // console.log(message);
      // console.log(chunk);
      const result = JSON.parse(response);
      console.log(result.success, this.skip);
      const user = {
        identifier: file.uniqueIdentifier,
        filename: file.name,
        totalChunks: chunk.offset,
      }
      if (result.success && !this.skip) {
        upload(user).then((res) => {
          if (res.code == 200) {
            console.log("上传成功");
          } else {
            console.log(res);
          }
        })
            .catch(function (error) {
              console.log(error);
            });
      } else {
        console.log("上传成功,不需要合并");
      }
      if (this.skip) {
        this.skip = false;
      }
    },
    fileComplete(rootFile) {
      // 一个根文件(文件夹)成功上传完成。
      // console.log("fileComplete", rootFile);
      // console.log("一个根文件(文件夹)成功上传完成。");
    },
    complete() {
      // 上传完毕。
      // console.log("complete");
    },
    filesAdded(file, fileList, event) {
      // console.log(file);
      file.forEach((e) => {
        this.fileList.push(e);
        this.computeMD5(e);
      });
    },
    computeMD5(file) {
      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 = 1024 * 1024;
      let chunks = Math.ceil(file.size / chunkSize);
      let spark = new SparkMD5.ArrayBuffer();
      // 文件状态设为"计算MD5"
      file.cmd5 = true; //文件状态为“计算md5...”
      file.pause();
      loadNext();
      fileReader.onload = (e) => {
        spark.append(e.target.result);
        if (currentChunk < chunks) {
          currentChunk++;
          loadNext();
          // 实时展示MD5的计算进度
          console.log(
              `第${currentChunk}分片解析完成, 开始第${
                  currentChunk + 1
              } / ${chunks}分片解析`
          );
        } else {
          let md5 = spark.end();
          console.log(
              `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${
                  file.size
              } 用时:${new Date().getTime() - time} ms`
          );
          spark.destroy(); //释放缓存
          file.uniqueIdentifier = md5; //将文件md5赋值给文件唯一标识
          file.cmd5 = false; //取消计算md5状态
          file.resume(); //开始上传
        }
      };
      fileReader.onerror = function () {
        this.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));
      }
    },
    allStart() {
      console.log(this.fileList);
      this.fileList.map((e) => {
        if (e.paused) {
          e.resume();
        }
      });
    },
    allStop() {
      console.log(this.fileList);
      this.fileList.map((e) => {
        if (!e.paused) {
          e.pause();
        }
      });
    },
    allRemove() {
      this.fileList.map((e) => {
        e.cancel();
      });
      this.fileList = [];
    },
  },
};
</script>

<style>
.uploader-example {
  width: 100%;
  padding: 15px;
  margin: 0px auto 0;
  font-size: 12px;
  box-shadow: 0 0 10px rgba(0, 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>

效果:

 

 

 redis 中的文件:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/495648.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2023年博客之星入围选拔重装开启——今年没有拉票环节啦

2023年博客之星入围选拔开始啦&#xff01; 2022的博客之星大赛言犹在耳&#xff0c;2023的活动又接踵而至了。今年的博客之星&#xff08;包括博客新星&#xff09;的入围规则很简单&#xff1a; 2023年新规则 1.不需要拉票&#xff01;不喜欢这个环节的有福啦 2.从年初就…

python对医疗数据进行分析,看看哪个年龄段最容易生病

前言 大家早好、午好、晚好吖 ❤ ~欢迎光临本文章 什么是数据分析 明确目的–获得数据(爬虫&#xff0c;现有&#xff0c;公开的数据)–数据预处理——数据可视化——结论 准备 环境使用&#xff1a; 在开始写我们的代码之前&#xff0c;我们要准备好运行代码的程序 Anacon…

附录3-动态组件 component与keep-alive setup()写法

目录 1 基本使用 2 按钮切换组件 3 keep-alive 3.1 在切换组件的时候&#xff0c;默认情况下被切换的组件就会被销毁 3.1.1 数据情况 3.1.2 生命周期函数情况 3.2 使用 keep-alive 3.3 keep-alive的生命周期函数 3.4 缓存指定组件 include 3.5 不缓存指定组…

Redis学习——day02

Redis学习基础 性能测试工具&#xff1a;Redis-benchmarkRedis基础知识&#xff1a;redis的数据类型&#xff1a;特殊数据类型&#xff1a; 性能测试工具&#xff1a;Redis-benchmark 官方自带的测试工具参数&#xff1a;简单练习&#xff1a;redis-benchmark -h localhost -p…

前端开发环境搭建踩坑笔记——npm install node-sass安装失败的解决方案

文章目录 问题背景问题描述解决方案1. 检查node-sass安装版本是否正确。2. 检查是否是网络问题 总结 问题背景 开发新的前端项目时&#xff0c;总少不了搭建开发环境和执行npm install安装依赖包&#xff0c;但npm install的过程总是充满着玄学&#xff0c;很难保证一次性成功…

Java之多线程初阶2

一.上节内容复习 上节内容指路:Java之多线程初阶 1.进程和线程的区别 1.进程中至少包含一个线程(主线程) 2.进程是申请计算机资源的最小单位 3.线程是CPU调度的最小单位 4.进程之间是相互隔离的,线程是使用的是进程统一申请来的资源,之间可以相互影响 2.创建线程的四种方…

设计模式——适配器模式(类适配器、对象适配器)

是什么&#xff1f; 我们平时的有线耳机接口分为USB的和Type-C的接口&#xff0c;但是手机的耳机插口却只有一个&#xff0c;像华为的耳机插口现在基本都是Type-c的&#xff0c;那如果我们现在只有USB接口的耳机怎么办呢&#xff0c;这个时候就需要使用到一个转换器&#xff0c…

数据汇总从20s优化至2s(小经验)

目录 项目背景&#xff1a; 数据汇总访问时间长的问题&#xff1a; 解决方案&#xff1a; 1.创建参与访问表 2.使用redis进行优化 总结&#xff1a; 项目背景&#xff1a; 一个简单的抽奖系统&#xff0c;当要统计每天的参与访问&#xff0c;总的参与访问 数据汇总访问时…

运维高可用架构的 6 大常规方案

在介绍高可用架构的方案之前&#xff0c;先说一下什么是高可用架构&#xff0c;高可用架构应具备但不限于以下特征&#xff1a; 主从切换 很好理解&#xff0c;当其中一台机器的服务宕机后&#xff0c;对于服务调用者来说&#xff0c;能够迅速的切换到其他可用服务&#xff0c;…

俩小伙一晚上写了个 AI 应用,月入两万??(文末附开发教程)

开发出一款能够与 AI 对话生成和编辑思维导图的工具&#xff0c;听起来似乎只能是一群专业的 AI 背景团队花费大量的时间和精力训练模型&#xff0c;打磨应用才能完成的事情。 但是&#xff0c;两名大学生却在一夜之间完成了&#xff0c;就像炼金术士将庸俗的材料转化成黄金一…

吃掉脂肪,狂减33斤!午餐菜单一次性公开!

一周高效减脂蔬菜沙拉&#xff5c;上班族学生党减脂备餐便当 &#x1f618;我是4个月瘦30斤&#xff0c;减脂迫在眉睫&#xff0c;巨掉秤好吃的蔬菜沙拉来咯&#xff01;肉类提前备好放冷冻&#xff0c;工作日只需10分钟搞定&#xff0c;做减脂早午晚餐都可以哦&#xff5e; …

Mysql 中left join时 on、and、where区别

1、准备两张表student与class表 student class 2、left join on左连接 select * from student s left join class c on s.classId c.id 左表数据全部显示&#xff0c;关联到的右表数据显示&#xff0c;没有显示null 3、left join on ... and对左表student进行条件筛选 …

解决echarts图表随窗口宽度变化而改变图表的大小

文章目录 前言一、演示前后对比效果二、解決方法1.在代码结尾加上监听方法2.示例 三、总结扩展问题 前言 很多同学在使用echarts时遇到了浏览器窗口大小发生变化时&#xff0c;图表大小没有自适应窗口的宽度&#xff0c;下面我将对比演示随着窗口大小变化&#xff0c;echarts图…

(四)【平衡小车制作】陀螺仪MPU6050

一、硬件结构 1.什么是陀螺仪&#xff1f; 陀螺仪是用于测量或维护方位和角速度的设备。它是一个旋转的轮子或圆盘&#xff0c;其中旋转轴可以不受影响的设定在任何方向。当旋转发生时&#xff0c;根据角动量守恒定律&#xff0c;该轴的方向不受支架倾斜或旋转的影响。 2.MPU…

在Ubuntu18.04中安装uWebSockets库

目录 1.下载uWebSockets库2.下载uSockets3.安装openssl开发包4.编译首先说明这里使用的Ubuntu版本为18.04。 1.下载uWebSockets库 下载uWebSockets库有两种方式,一是终端,从Github中克隆uWebSockets库到Ubuntu本地文件夹,二是打开uWebSockets库下载链接自己下载到Windows,然…

Python实现将快手个人主页的视频进行无水印下载

前言 本文是该专栏的第26篇,后面会持续分享python的各种干货知识,值得关注。 笔者之前有详细介绍快手滑块验证码的识别方法(Python如何解决“快手滑块验证码”(4)),感兴趣的同学可往前翻阅查看。需要注意的是,滑块验证码的缺失距离需要根据你自己电脑的分辨率进行微调,…

STM32 i2c 驱动0.42寸OLED

STM32 i2c 驱动0.42寸OLED&#xff0c;这是个项目中使用的方案&#xff0c;调试也是比较费劲&#xff0c;主要是取字模和代码的匹配&#xff0c;下个篇章中详解一下取字模的过程&#xff0c;在这个方案中的使用。 本文使用的测试代码 STM32i2c驱动0.42寸OLED&#xff0c;SSD1…

体验 ChatGLM-6B

体验 ChatGLM-6B 1. 什么是 ChatGLM-6B2. Github 地址3. 安装 Miniconda34. 创建虚拟环境5. 安装 ChatGLM-6B6. 启动 ChatGLM-6B7. 访问 ChatGLM-6B8. API部署9. 命令行部署10. 其他&#xff0c;修改使用显存大小 1. 什么是 ChatGLM-6B ChatGLM-6B 是一个开源的、支持中英双语…

面了一个4年经验的测试工程师,自动化都不会也要18k,我真是醉了...

在深圳这家金融公司也待了几年&#xff0c;被别人面试过也面试过别人&#xff0c;大大小小的事情也见识不少&#xff0c;今天又是团面的一天&#xff0c; 一百多个人都聚集在一起&#xff0c;因为公司最近在谈项目出来面试就2个人&#xff0c;无奈又被叫到面试房间。 整个过程…

音频焦点使用及原理

音频焦点使用及原理 本博客代码基于Android 10源码 为什么会有音频焦点这一概念&#xff1f; 在Android音频领域中&#xff0c;应用层所有的App播放音频&#xff0c;最终都是走到音频回播线程PlaybackThread中&#xff0c;如果多个App都走到同一个PlaybackThread中去&#xff0…