searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

断点续传

2024-08-29 09:42:17
36
0

前言

分片: 分片任务是在前端由vue-simple-uploader插件完成,流程:1.前端先发送check-file(检查文件MD5)来确认文件是直接秒传还是分片上传,如果计算出文件所有片已经上传完成,则启用秒传(秒传就是不传),如果是新文件,则需要分片上传,由vue-simple-uploader插件将文件按固定大小进行切割,然后逐片上传。

断点续传: 意思就是一个大文件分了多少片,这些片已经上传了哪些,还有哪些没上传,这些都会记录在文件存储目录下的.conf文件中,当你上传大文件时,传一部分后刷新浏览器或关闭浏览器,这时候传输会中断,然后你再打开页面重新上传该文件,它会先检测还有哪些片没有上传,然后直接上传的上次未传的片,这就是断点续传。

秒传: 文件不经过上传的步骤,直接将文件信息保存在服务器中。通过计算文件md5实现

流程

  1. 校验文件上传状态: 前端生成该文件的MD5密文并进行分片,上传之前请求check-md5接口,传入文件名和密文,接口校验文件是未上传上传了一部分已上传完成三个状态,其中未上传返回自定义状态码404,上传一部分则返回状态206+未上传的分片ID,上传完成则返回状态200
  2. 前端逐片上传: 校验完成后,根据校验结果对未上传的分片进行逐个上传,上传分片时参数主要是:总片数、当前片ID、片文件
  3. 上传接口: 上传接口会先去获取并解析该文件的conf文件(conf文件是RandomAccessFile,该类是通过提供指针的方式操作文件,文件存储的是一个二进制数组,所以可以用来数组下标标记片ID),使用setLength方法设置conf文件长度,使用seek方法跳到当前上传的片ID的位置,把该位置的值替换成127,然后将该分片使用指针偏移的方式插入到_tmp临时文件(临时文件也是RandomAccessFile文件)中,然后校验是否所有的片都上传完成,是则修改临时文件为正式文件名,至此上传完成,否则直接返回该分片上传完成
  4. 上传进度: 前端收到当前片的响应结果后,会根据已上传片数量获取到上传进度
  5. MD5的用法: 用于计算服务器是否已经存在相同md5的文件,用作秒传功能的实现。前端计算文件md5,传入后端进行查找是否已经有相同md5文件,若存在直接返回上传成功,否则走上传的步骤

后端接口

Controller

/**
 * 检查文件MD5(文件MD5若已存在进行秒传)
 *
 * @param md5      md5
 * @param fileName 文件名称
 * @return {@link ApiResponse}
 * @author 7bin
 **/
@GetMapping(value = "/check")
public ApiResponse checkFileMd5(String md5, String fileName) {
    // Result result = fileService.checkFileMd5(md5, fileName);
    // return NovelWebUtils.forReturn(result);
    return ApiResponse.success();
}

/**
 * 断点续传方式上传文件:用于大文件上传
 *
 * @param chunkDTO   参数
 * @param request 请求
 * @return {@link ApiResponse}
 * @author 7bin
 **/
@PostMapping(value = "/breakpoint-upload", consumes = "multipart/*", headers = "content-type=multipart/form-data", produces = "application/json;charset=UTF-8")
public ApiResponse breakpointResumeUpload(Chunk chunkDTO, HttpServletRequest request) {

    String id = chunkDTO.getIdentifier();
    int chunks = Math.toIntExact(chunkDTO.getTotalChunks());
    int chunk = chunkDTO.getChunkNumber() - 1;
    long size = chunkDTO.getCurrentChunkSize();
    String name = chunkDTO.getFilename();
    MultipartFile file = chunkDTO.getFile();
    String md5 = chunkDTO.getIdentifier();
    UploadFileParam param = new UploadFileParam(id, chunks, chunk, size, name, file, md5);

    // return ApiResponse.success();
    Result result = fileService.breakpointResumeUpload(param, request);
    return NovelWebUtils.forReturn(result);
}

/**
 * 检查文件MD5(文件MD5若已存在进行秒传)
 * @param chunkMap
 * @return {@link ApiResponse}
 * @author 7bin
 **/
@GetMapping(value = "/breakpoint-upload")
public ApiResponse breakpointResumeUploadPre(
    @RequestParam Map<String, String> chunkMap) {

    String md5 = chunkMap.get("identifier");
    String filename = chunkMap.get("filename");
    Result<JSONArray> result = fileService.checkFileMd5(md5, filename);


    JSONObject res = new JSONObject();

    // 数据库中存在该md5则秒传
    if (result == null){
        res.put("skipUpload",true);
        return ApiResponse.success(res);
    }

    boolean skipUpload = false;
    if ("200".equals(result.getCode()) || "201".equals(result.getCode())) {
        skipUpload = true;
    } else if ("206".equals(result.getCode())) {
        // 已经上传部分分块
        // data中存放的是还未上传的分块
        JSONArray data = result.getData();
        res.put("missChunks",data);
    }

    res.put("skipUpload",skipUpload);

    return ApiResponse.success(res);
    // Result result = fileService.breakpointResumeUpload(param, request);
    // return NovelWebUtils.forReturn(result);
}

Service

@Override
public Result<JSONArray> checkFileMd5(String md5, String fileName) {

    boolean exist = fileMapper.fileIsExist(md5);
    if (exist){
        return null;
    }

    Result<JSONArray> result;
    try {
        // String realFilename = md5 + "_" + fileName;
        String realFilename = md5;
        result = LocalUpload.checkFileMd5(md5, realFilename, confFilePath, savePath);
    } catch (Exception e) {
        // e.printStackTrace();
        log.error(e.getMessage());
        throw new ServiceException(e.getMessage());
    }
    return result;

}

@Override
public Result breakpointResumeUpload(UploadFileParam param, HttpServletRequest request) {
    Result result;
    try {
        // 这里的 chunkSize(分片大小) 要与前端传过来的大小一致
        // long chunkSize = Objects.isNull(param.getChunkSize()) ? 5 * 1024 * 1024
        //     : param.getChunkSize();

        // 实际存储的文件格式为 [{md5}_{filename}]
        // String realFilename = param.getMd5() + "_" + param.getName();
        String realFilename = param.getMd5();
        param.setName(realFilename);
        result = LocalUpload.fragmentFileUploader(param, confFilePath, savePath, 5242880L, request);
        // return NovelWebUtils.forReturn(result);
    } catch (Exception e) {
        log.error(e.getMessage());
        throw new ServiceException(e.getMessage());
    }
    return result;
}

前端代码

SimpleUploader.vue

<template>
  <div id="global-uploader">
    <uploader
      class="uploader-app"
      :options="initOptions"
      :file-status-text="fileStatusText"
      :auto-start="false"
      @file-added="onFileAdded"
      @file-success="onFileSuccess"
      @file-progress="onFileProgress"
      @file-error="onFileError"
    >
      <uploader-unsupport></uploader-unsupport>

      <uploader-drop>
        <uploader-btn >选择文件</uploader-btn>
        <span style="margin-left: 10px">(支持上传一个或多个文件)</span>
        <!--<uploader-btn directory>上传文件夹 </uploader-btn>-->
      </uploader-drop>
      <!--<uploader-btn id="global-uploader-btn" ref="uploadBtnRef">选择文件</uploader-btn>-->
      <!--<span>(支持上传一个或多个文件)</span>-->


      <uploader-list>
        <template #default="{ fileList }">
          <div class="file-panel">
            <!--<div class="file-title">-->
            <!--  <div class="title">文件列表</div>-->
            <!--</div>-->

            <ul class="file-list">
              <li
                  v-for="file in fileList"
                  :key="file.id"
                  class="file-item"
              >
                <uploader-file
                    ref="files"
                    :class="['file_' + file.id, customStatus]"
                    :file="file"
                    :list="true"
                ></uploader-file>
              </li>
              <div v-if="!fileList.length" class="no-file">
                <!--<Icon icon="ri:file-3-line" width="16" /> 暂无待上传文件-->
                暂无待上传文件
              </div>
            </ul>
          </div>
        </template>
      </uploader-list>
    </uploader>
  </div>
</template>

<script setup>
import useCurrentInstance from "@/utils/currentInstance";
import { generateMD5 } from "@/components/Uploader/utils/md5";
import { ElNotification } from "element-plus";
import { addFileToDrive } from "@/api/drive/drive";
import { checkAuth } from "@/api/admin/user";
const { proxy } = useCurrentInstance();


// TODO 上传组件还有bug 上传成功时动作按钮没有隐藏;后端出现错误上传失败时背景色没变红

// props


// emits
const emits = defineEmits(['uploadSuccess']);


const drivePath = import.meta.env.VITE_APP_DRIVE_API;

const initOptions = {
  target: drivePath + "/file/breakpoint-upload",
  chunkSize: '5242880',
  forceChunkSize: true,
  fileParameterName: 'file',
  maxChunkRetries: 3,
  // 是否开启服务器分片校验
  testChunks: true,
  // 服务器分片校验函数,秒传及断点续传基础
  checkChunkUploadedByResponse: function (chunk, message) {
    let skip = false
    // console.log("checkChunkUploadedByResponse chunk:", chunk);
    // console.log("checkChunkUploadedByResponse message:", message);
    try {
      let objMessage = JSON.parse(message)
      // console.log("objMessage:", objMessage);
      if (objMessage.code === 200) {
        if (objMessage.data.skipUpload) {
          skip = true
        } else if (objMessage.data.missChunks == null){
          skip = false;
        } else {
          skip = (objMessage.data.missChunks || []).indexOf(chunk.offset.toString()) < 0
        }
      }

    } catch (e) {}
    // console.log("skip: " + chunk.offset + " " + skip);
    return skip
  },
  query: (file, chunk) => {
    // console.log("query:", file);
    return {
      ...file.params
    }
  }
}

const customStatus = ref('')

const fileStatusText = {
  success: '上传成功',
  error: '上传失败',
  uploading: '上传中',
  paused: '已暂停',
  waiting: '等待上传'
}

// const uploaderRef = ref()
// const uploader = computed(() => uploaderRef.value?.uploader)

async function onFileAdded(file) {
  // 判断用户是否已经登录了,登录才可以添加
  await checkAuth();

  // 暂停文件
  // 选择文件后暂停文件上传,上传时手动启动
  file.pause()
  // console.log("onFileAdded file: ", file);
  // panelShow.value = true
  // trigger('fileAdded')
  // 将额外的参数赋值到每个文件上,以不同文件使用不同params的需求
  // file.params = customParams.value
  // 计算MD5
  const md5 = await computeMD5(file)
  startUpload(file, md5)
}
function computeMD5(file) {
  // 文件状态设为"计算MD5"
  statusSet(file.id, 'md5')

  // 计算MD5时隐藏"开始"按钮
  nextTick(() => {
    // document.querySelector(`.file_${file.id} .uploader-file-resume`).style.display = 'none'
    document.querySelector(`.file_${file.id} .uploader-file-actions`).style.display = 'none'
  })
  // 开始计算MD5
  return new Promise((resolve, reject) => {
    generateMD5(file, {
      onProgress(currentChunk, chunks) {
        // 实时展示MD5的计算进度
        nextTick(() => {
          const md5ProgressText = '校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%'
          document.querySelector(`.custom-status-${file.id}`).innerText = md5ProgressText
        })
      },
      onSuccess(md5) {
        statusRemove(file.id)
        resolve(md5)
      },
      onError() {
        error(`文件${file.name}读取出错,请检查该文件`)
        file.cancel()
        statusRemove(file.id)
        reject()
      }
    })
  })
}
// md5计算完毕,开始上传
function startUpload(file, md5) {
  file.uniqueIdentifier = md5
  file.resume()
}

function onFileProgress(rootFile, file, chunk) {
  console.log(
    `上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${
      chunk.endByte / 1024 / 1024
    }`
  )
}

const onFileError = (rootFile, file, response, chunk) => {
  // console.log('error', file)
  error(response)
}
function error(msg) {
  ElNotification({
    title: '错误',
    message: msg,
    type: 'error',
    duration: 2000
  })
}
const onFileSuccess = (rootFile, file, response, chunk) => {
  // console.log("上传成功")
  // console.log("rootFile",rootFile)
  // file的relativePath是文件夹的相对路径(如果上传的是文件夹的话)
  // console.log("file",file)
  // console.log("response",JSON.parse(response))
  // console.log("chunk",chunk)
  // addFileToDrive(file.name, file.uniqueIdentifier, file.size).then(() => {
  //   proxy.$modal.msgSuccess("文件上传成功");
  // })

  // 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
  let res = JSON.parse(response)
  console.log("onFileSuccess res:", res);
  if (res.code !== 200) {
    error(res.message)
    // 文件状态设为“失败”
    statusSet(file.id, 'failed')
    return
  }

  emits("uploadSuccess", file);
}


/**
 * 新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
 * @param id
 * @param status
 */
function statusSet(id, status) {
  const statusMap = {
    md5: {
      text: '校验MD5',
      bgc: '#fff'
    },
    failed: {
      text: '上传失败',
      bgc: '#e2eeff'
    }
  }

  customStatus.value = status
  nextTick(() => {
    const statusTag = document.createElement('span')
    statusTag.className = `custom-status-${id} custom-status`
    statusTag.innerText = statusMap[status].text
    statusTag.style.backgroundColor = statusMap[status].bgc

    // custom-status 样式不生效
    // 由于 style脚本 设置了 scoped,深层的样式修改不了
    // 通过给当前组件设置一个id,在该id下设置样式,就可以保证样式不全局污染
    // statusTag.style.position = 'absolute';
    // statusTag.style.top = '0';
    // statusTag.style.left = '0';
    // statusTag.style.right = '0';
    // statusTag.style.bottom = '0';
    // statusTag.style.zIndex = '1';

    const statusWrap = document.querySelector(`.file_${id} .uploader-file-status`)
    statusWrap.appendChild(statusTag)
  })
}
function statusRemove(id) {
  customStatus.value = ''
  nextTick(() => {
    const statusTag = document.querySelector(`.custom-status-${id}`)
    document.querySelector(`.file_${id} .uploader-file-actions`).style.display = 'block'
    statusTag.remove()
  })
}
</script>

md5.js

import SparkMD5 from 'spark-md5'

/**
 * 分段计算MD5
 * @param file {File}
 * @param options {Object} - onProgress | onSuccess | onError
 */
export function generateMD5(file, options = {}) {
  const fileReader = new FileReader()
  const time = new Date().getTime()
  const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
  const chunkSize = 10 * 1024 * 1000
  const chunks = Math.ceil(file.size / chunkSize)
  let currentChunk = 0
  const spark = new SparkMD5.ArrayBuffer()
  const loadNext = () => {
    let start = currentChunk * chunkSize
    let end = start + chunkSize >= file.size ? file.size : start + chunkSize

    fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
  }

  loadNext()

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

    if (currentChunk < chunks) {
      currentChunk++
      loadNext()
      if (options.onProgress && typeof options.onProgress == 'function') {
        options.onProgress(currentChunk, chunks)
      }
    } else {
      let md5 = spark.end()

      // md5计算完毕
      if (options.onSuccess && typeof options.onSuccess == 'function') {
        options.onSuccess(md5)
      }

      console.log(
        `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${
          new Date().getTime() - time
        } ms`
      )
    }
  }

  fileReader.onerror = function () {
    console.log('MD5计算失败')
    if (options.onError && typeof options.onError == 'function') {
      options.onError()
    }
  }
}
0条评论
0 / 1000
陈****彬
6文章数
1粉丝数
陈****彬
6 文章 | 1 粉丝
陈****彬
6文章数
1粉丝数
陈****彬
6 文章 | 1 粉丝
原创

断点续传

2024-08-29 09:42:17
36
0

前言

分片: 分片任务是在前端由vue-simple-uploader插件完成,流程:1.前端先发送check-file(检查文件MD5)来确认文件是直接秒传还是分片上传,如果计算出文件所有片已经上传完成,则启用秒传(秒传就是不传),如果是新文件,则需要分片上传,由vue-simple-uploader插件将文件按固定大小进行切割,然后逐片上传。

断点续传: 意思就是一个大文件分了多少片,这些片已经上传了哪些,还有哪些没上传,这些都会记录在文件存储目录下的.conf文件中,当你上传大文件时,传一部分后刷新浏览器或关闭浏览器,这时候传输会中断,然后你再打开页面重新上传该文件,它会先检测还有哪些片没有上传,然后直接上传的上次未传的片,这就是断点续传。

秒传: 文件不经过上传的步骤,直接将文件信息保存在服务器中。通过计算文件md5实现

流程

  1. 校验文件上传状态: 前端生成该文件的MD5密文并进行分片,上传之前请求check-md5接口,传入文件名和密文,接口校验文件是未上传上传了一部分已上传完成三个状态,其中未上传返回自定义状态码404,上传一部分则返回状态206+未上传的分片ID,上传完成则返回状态200
  2. 前端逐片上传: 校验完成后,根据校验结果对未上传的分片进行逐个上传,上传分片时参数主要是:总片数、当前片ID、片文件
  3. 上传接口: 上传接口会先去获取并解析该文件的conf文件(conf文件是RandomAccessFile,该类是通过提供指针的方式操作文件,文件存储的是一个二进制数组,所以可以用来数组下标标记片ID),使用setLength方法设置conf文件长度,使用seek方法跳到当前上传的片ID的位置,把该位置的值替换成127,然后将该分片使用指针偏移的方式插入到_tmp临时文件(临时文件也是RandomAccessFile文件)中,然后校验是否所有的片都上传完成,是则修改临时文件为正式文件名,至此上传完成,否则直接返回该分片上传完成
  4. 上传进度: 前端收到当前片的响应结果后,会根据已上传片数量获取到上传进度
  5. MD5的用法: 用于计算服务器是否已经存在相同md5的文件,用作秒传功能的实现。前端计算文件md5,传入后端进行查找是否已经有相同md5文件,若存在直接返回上传成功,否则走上传的步骤

后端接口

Controller

/**
 * 检查文件MD5(文件MD5若已存在进行秒传)
 *
 * @param md5      md5
 * @param fileName 文件名称
 * @return {@link ApiResponse}
 * @author 7bin
 **/
@GetMapping(value = "/check")
public ApiResponse checkFileMd5(String md5, String fileName) {
    // Result result = fileService.checkFileMd5(md5, fileName);
    // return NovelWebUtils.forReturn(result);
    return ApiResponse.success();
}

/**
 * 断点续传方式上传文件:用于大文件上传
 *
 * @param chunkDTO   参数
 * @param request 请求
 * @return {@link ApiResponse}
 * @author 7bin
 **/
@PostMapping(value = "/breakpoint-upload", consumes = "multipart/*", headers = "content-type=multipart/form-data", produces = "application/json;charset=UTF-8")
public ApiResponse breakpointResumeUpload(Chunk chunkDTO, HttpServletRequest request) {

    String id = chunkDTO.getIdentifier();
    int chunks = Math.toIntExact(chunkDTO.getTotalChunks());
    int chunk = chunkDTO.getChunkNumber() - 1;
    long size = chunkDTO.getCurrentChunkSize();
    String name = chunkDTO.getFilename();
    MultipartFile file = chunkDTO.getFile();
    String md5 = chunkDTO.getIdentifier();
    UploadFileParam param = new UploadFileParam(id, chunks, chunk, size, name, file, md5);

    // return ApiResponse.success();
    Result result = fileService.breakpointResumeUpload(param, request);
    return NovelWebUtils.forReturn(result);
}

/**
 * 检查文件MD5(文件MD5若已存在进行秒传)
 * @param chunkMap
 * @return {@link ApiResponse}
 * @author 7bin
 **/
@GetMapping(value = "/breakpoint-upload")
public ApiResponse breakpointResumeUploadPre(
    @RequestParam Map<String, String> chunkMap) {

    String md5 = chunkMap.get("identifier");
    String filename = chunkMap.get("filename");
    Result<JSONArray> result = fileService.checkFileMd5(md5, filename);


    JSONObject res = new JSONObject();

    // 数据库中存在该md5则秒传
    if (result == null){
        res.put("skipUpload",true);
        return ApiResponse.success(res);
    }

    boolean skipUpload = false;
    if ("200".equals(result.getCode()) || "201".equals(result.getCode())) {
        skipUpload = true;
    } else if ("206".equals(result.getCode())) {
        // 已经上传部分分块
        // data中存放的是还未上传的分块
        JSONArray data = result.getData();
        res.put("missChunks",data);
    }

    res.put("skipUpload",skipUpload);

    return ApiResponse.success(res);
    // Result result = fileService.breakpointResumeUpload(param, request);
    // return NovelWebUtils.forReturn(result);
}

Service

@Override
public Result<JSONArray> checkFileMd5(String md5, String fileName) {

    boolean exist = fileMapper.fileIsExist(md5);
    if (exist){
        return null;
    }

    Result<JSONArray> result;
    try {
        // String realFilename = md5 + "_" + fileName;
        String realFilename = md5;
        result = LocalUpload.checkFileMd5(md5, realFilename, confFilePath, savePath);
    } catch (Exception e) {
        // e.printStackTrace();
        log.error(e.getMessage());
        throw new ServiceException(e.getMessage());
    }
    return result;

}

@Override
public Result breakpointResumeUpload(UploadFileParam param, HttpServletRequest request) {
    Result result;
    try {
        // 这里的 chunkSize(分片大小) 要与前端传过来的大小一致
        // long chunkSize = Objects.isNull(param.getChunkSize()) ? 5 * 1024 * 1024
        //     : param.getChunkSize();

        // 实际存储的文件格式为 [{md5}_{filename}]
        // String realFilename = param.getMd5() + "_" + param.getName();
        String realFilename = param.getMd5();
        param.setName(realFilename);
        result = LocalUpload.fragmentFileUploader(param, confFilePath, savePath, 5242880L, request);
        // return NovelWebUtils.forReturn(result);
    } catch (Exception e) {
        log.error(e.getMessage());
        throw new ServiceException(e.getMessage());
    }
    return result;
}

前端代码

SimpleUploader.vue

<template>
  <div id="global-uploader">
    <uploader
      class="uploader-app"
      :options="initOptions"
      :file-status-text="fileStatusText"
      :auto-start="false"
      @file-added="onFileAdded"
      @file-success="onFileSuccess"
      @file-progress="onFileProgress"
      @file-error="onFileError"
    >
      <uploader-unsupport></uploader-unsupport>

      <uploader-drop>
        <uploader-btn >选择文件</uploader-btn>
        <span style="margin-left: 10px">(支持上传一个或多个文件)</span>
        <!--<uploader-btn directory>上传文件夹 </uploader-btn>-->
      </uploader-drop>
      <!--<uploader-btn id="global-uploader-btn" ref="uploadBtnRef">选择文件</uploader-btn>-->
      <!--<span>(支持上传一个或多个文件)</span>-->


      <uploader-list>
        <template #default="{ fileList }">
          <div class="file-panel">
            <!--<div class="file-title">-->
            <!--  <div class="title">文件列表</div>-->
            <!--</div>-->

            <ul class="file-list">
              <li
                  v-for="file in fileList"
                  :key="file.id"
                  class="file-item"
              >
                <uploader-file
                    ref="files"
                    :class="['file_' + file.id, customStatus]"
                    :file="file"
                    :list="true"
                ></uploader-file>
              </li>
              <div v-if="!fileList.length" class="no-file">
                <!--<Icon icon="ri:file-3-line" width="16" /> 暂无待上传文件-->
                暂无待上传文件
              </div>
            </ul>
          </div>
        </template>
      </uploader-list>
    </uploader>
  </div>
</template>

<script setup>
import useCurrentInstance from "@/utils/currentInstance";
import { generateMD5 } from "@/components/Uploader/utils/md5";
import { ElNotification } from "element-plus";
import { addFileToDrive } from "@/api/drive/drive";
import { checkAuth } from "@/api/admin/user";
const { proxy } = useCurrentInstance();


// TODO 上传组件还有bug 上传成功时动作按钮没有隐藏;后端出现错误上传失败时背景色没变红

// props


// emits
const emits = defineEmits(['uploadSuccess']);


const drivePath = import.meta.env.VITE_APP_DRIVE_API;

const initOptions = {
  target: drivePath + "/file/breakpoint-upload",
  chunkSize: '5242880',
  forceChunkSize: true,
  fileParameterName: 'file',
  maxChunkRetries: 3,
  // 是否开启服务器分片校验
  testChunks: true,
  // 服务器分片校验函数,秒传及断点续传基础
  checkChunkUploadedByResponse: function (chunk, message) {
    let skip = false
    // console.log("checkChunkUploadedByResponse chunk:", chunk);
    // console.log("checkChunkUploadedByResponse message:", message);
    try {
      let objMessage = JSON.parse(message)
      // console.log("objMessage:", objMessage);
      if (objMessage.code === 200) {
        if (objMessage.data.skipUpload) {
          skip = true
        } else if (objMessage.data.missChunks == null){
          skip = false;
        } else {
          skip = (objMessage.data.missChunks || []).indexOf(chunk.offset.toString()) < 0
        }
      }

    } catch (e) {}
    // console.log("skip: " + chunk.offset + " " + skip);
    return skip
  },
  query: (file, chunk) => {
    // console.log("query:", file);
    return {
      ...file.params
    }
  }
}

const customStatus = ref('')

const fileStatusText = {
  success: '上传成功',
  error: '上传失败',
  uploading: '上传中',
  paused: '已暂停',
  waiting: '等待上传'
}

// const uploaderRef = ref()
// const uploader = computed(() => uploaderRef.value?.uploader)

async function onFileAdded(file) {
  // 判断用户是否已经登录了,登录才可以添加
  await checkAuth();

  // 暂停文件
  // 选择文件后暂停文件上传,上传时手动启动
  file.pause()
  // console.log("onFileAdded file: ", file);
  // panelShow.value = true
  // trigger('fileAdded')
  // 将额外的参数赋值到每个文件上,以不同文件使用不同params的需求
  // file.params = customParams.value
  // 计算MD5
  const md5 = await computeMD5(file)
  startUpload(file, md5)
}
function computeMD5(file) {
  // 文件状态设为"计算MD5"
  statusSet(file.id, 'md5')

  // 计算MD5时隐藏"开始"按钮
  nextTick(() => {
    // document.querySelector(`.file_${file.id} .uploader-file-resume`).style.display = 'none'
    document.querySelector(`.file_${file.id} .uploader-file-actions`).style.display = 'none'
  })
  // 开始计算MD5
  return new Promise((resolve, reject) => {
    generateMD5(file, {
      onProgress(currentChunk, chunks) {
        // 实时展示MD5的计算进度
        nextTick(() => {
          const md5ProgressText = '校验MD5 ' + ((currentChunk / chunks) * 100).toFixed(0) + '%'
          document.querySelector(`.custom-status-${file.id}`).innerText = md5ProgressText
        })
      },
      onSuccess(md5) {
        statusRemove(file.id)
        resolve(md5)
      },
      onError() {
        error(`文件${file.name}读取出错,请检查该文件`)
        file.cancel()
        statusRemove(file.id)
        reject()
      }
    })
  })
}
// md5计算完毕,开始上传
function startUpload(file, md5) {
  file.uniqueIdentifier = md5
  file.resume()
}

function onFileProgress(rootFile, file, chunk) {
  console.log(
    `上传中 ${file.name},chunk:${chunk.startByte / 1024 / 1024} ~ ${
      chunk.endByte / 1024 / 1024
    }`
  )
}

const onFileError = (rootFile, file, response, chunk) => {
  // console.log('error', file)
  error(response)
}
function error(msg) {
  ElNotification({
    title: '错误',
    message: msg,
    type: 'error',
    duration: 2000
  })
}
const onFileSuccess = (rootFile, file, response, chunk) => {
  // console.log("上传成功")
  // console.log("rootFile",rootFile)
  // file的relativePath是文件夹的相对路径(如果上传的是文件夹的话)
  // console.log("file",file)
  // console.log("response",JSON.parse(response))
  // console.log("chunk",chunk)
  // addFileToDrive(file.name, file.uniqueIdentifier, file.size).then(() => {
  //   proxy.$modal.msgSuccess("文件上传成功");
  // })

  // 服务端自定义的错误(即http状态码为200,但是是错误的情况),这种错误是Uploader无法拦截的
  let res = JSON.parse(response)
  console.log("onFileSuccess res:", res);
  if (res.code !== 200) {
    error(res.message)
    // 文件状态设为“失败”
    statusSet(file.id, 'failed')
    return
  }

  emits("uploadSuccess", file);
}


/**
 * 新增的自定义的状态: 'md5'、'merging'、'transcoding'、'failed'
 * @param id
 * @param status
 */
function statusSet(id, status) {
  const statusMap = {
    md5: {
      text: '校验MD5',
      bgc: '#fff'
    },
    failed: {
      text: '上传失败',
      bgc: '#e2eeff'
    }
  }

  customStatus.value = status
  nextTick(() => {
    const statusTag = document.createElement('span')
    statusTag.className = `custom-status-${id} custom-status`
    statusTag.innerText = statusMap[status].text
    statusTag.style.backgroundColor = statusMap[status].bgc

    // custom-status 样式不生效
    // 由于 style脚本 设置了 scoped,深层的样式修改不了
    // 通过给当前组件设置一个id,在该id下设置样式,就可以保证样式不全局污染
    // statusTag.style.position = 'absolute';
    // statusTag.style.top = '0';
    // statusTag.style.left = '0';
    // statusTag.style.right = '0';
    // statusTag.style.bottom = '0';
    // statusTag.style.zIndex = '1';

    const statusWrap = document.querySelector(`.file_${id} .uploader-file-status`)
    statusWrap.appendChild(statusTag)
  })
}
function statusRemove(id) {
  customStatus.value = ''
  nextTick(() => {
    const statusTag = document.querySelector(`.custom-status-${id}`)
    document.querySelector(`.file_${id} .uploader-file-actions`).style.display = 'block'
    statusTag.remove()
  })
}
</script>

md5.js

import SparkMD5 from 'spark-md5'

/**
 * 分段计算MD5
 * @param file {File}
 * @param options {Object} - onProgress | onSuccess | onError
 */
export function generateMD5(file, options = {}) {
  const fileReader = new FileReader()
  const time = new Date().getTime()
  const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice
  const chunkSize = 10 * 1024 * 1000
  const chunks = Math.ceil(file.size / chunkSize)
  let currentChunk = 0
  const spark = new SparkMD5.ArrayBuffer()
  const loadNext = () => {
    let start = currentChunk * chunkSize
    let end = start + chunkSize >= file.size ? file.size : start + chunkSize

    fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end))
  }

  loadNext()

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

    if (currentChunk < chunks) {
      currentChunk++
      loadNext()
      if (options.onProgress && typeof options.onProgress == 'function') {
        options.onProgress(currentChunk, chunks)
      }
    } else {
      let md5 = spark.end()

      // md5计算完毕
      if (options.onSuccess && typeof options.onSuccess == 'function') {
        options.onSuccess(md5)
      }

      console.log(
        `MD5计算完毕:${file.name} \nMD5:${md5} \n分片:${chunks} 大小:${file.size} 用时:${
          new Date().getTime() - time
        } ms`
      )
    }
  }

  fileReader.onerror = function () {
    console.log('MD5计算失败')
    if (options.onError && typeof options.onError == 'function') {
      options.onError()
    }
  }
}
文章来自个人专栏
扣顶
6 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
0
0