WebUploader实现分块上传(断点续传)着重后端Java代码的实现

前言

断点续传:
在下载或上传时,将文件分段,每段采用一个线程进行上传或下载。
每次上传分块前校验分块,如果已存在分块则不再上传。
因此若碰到网络故障中断,可继续上传下载(检测是否在服务器存在,或是否下载)。
最后将每段合并。


流程图

在这里插入图片描述


上传前准备工作

WebUploader.js中有给文件生成MD5的方法,即在文件上传前,把内容读取出来,算出MD5值,通过Ajax与服务端合并完成后的文件MD5值进行比对验证。

上传前还需要检查该上传的文件是否存在。

前端

         beforeSendFile:function(file) {
            // 创建一个deffered,用于通知是否完成操作
            var deferred = WebUploader.Deferred();
            // 计算文件的唯一标识,用于断点续传
            (new WebUploader.Uploader()).md5File(file, 0, 100*1024*1024)
              .then(function(val) {
                this.fileMd5 = val;
                this.uploadFile = file;
                //向服务端请求注册上传文件
                $.ajax(
                  {
                    type:"POST",
                    url:"/api/media/upload/register",
                    data:{
                      // 文件唯一表示
                      fileMd5:this.fileMd5,
                      fileName: file.name,
                      fileSize:file.size,
                      mimetype:file.type,
                      fileExt:file.ext
                    },
                    dataType:"json",
                    success:function(response) {
                      if(response.success) {
                        //alert('上传文件注册成功开始上传');
                        deferred.resolve();
                      } else {
                        alert(response.message);
                        deferred.reject();
                      }
                    }
                  }
                );
              }.bind(this));

            return deferred.promise();
          }.bind(this),

后端

    /**
     * 得到文件所属目录路径
     * 一级目录:MD5第一个字符
     * 二级目录:MD5第二个字符
     * 三级目录:MD5
     * 文件名:MD5+文件扩展名
     * @param fileMd5
     * @return
     */
    private String getFileFolderPath(String fileMd5) {
        return uploadLocation + fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
    }

    private String getFilePath(String fileMd5, String fileExt) {
        return this.getFileFolderPath(fileMd5) + fileMd5 + "." + fileExt;
    }

    private String getChunkFileFolderPath(String fileMd5) {
        return this.getFileFolderPath(fileMd5) + "chunk/";
    }
	
	// 文件上传前的准备工作
    public ResponseResult register(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        // 检查文件是否存在于磁盘
        String fileFolderPath = this.getFileFolderPath(fileMd5);
        String filePath = this.getFilePath(fileMd5, fileExt);
        File file = new File(filePath);
        boolean exists = file.exists();

        // 检查文件是否存在于MongoDB (主键为 fileMd5)
        boolean present = mediaFileRepository.findById(fileMd5).isPresent();
        if (exists && present) {
            // 既存在于磁盘又存在于数据库说明该文件存在
            ExceptionCast.cast(MediaCode.UPLOAD_FILE_REGISTER_EXIST);
        }
        // 文件不存在
        // 检查文件所在目录是否存在
        File fileFolder = new File(fileFolderPath);
        if (!fileFolder.exists()) {
            // 不存在创建该目录 (目录就是根据前端传来的MD5值创建的)
            fileFolder.mkdirs();
        }
        return ResponseResult.SUCCESS();
    }

检查分块是否存在

分块名是从0开始递增的(相当于索引),通过分块所在路径+分块的索引定位到具体分块来判断是否存在。
在这里插入图片描述
前端

        beforeSend:function(block) {
            var deferred = WebUploader.Deferred();
            // 每次上传分块前校验分块,如果已存在分块则不再上传,达到断点续传的目的
            $.ajax(
              {
                type:"POST",
                url:"/api/media/upload/checkchunk",
                data:{
                  // 文件唯一表示
                  fileMd5:this.fileMd5,
                  // 当前分块下标
                  chunk:block.chunk,
                  // 当前分块大小
                  chunkSize:block.end-block.start
                },
                dataType:"json",
                success:function(response) {
                  if(response.fileExist) {
                    // 分块存在,跳过该分块
                    deferred.reject();
                  } else {
                    // 分块不存在或不完整,重新发送
                    deferred.resolve();
                  }
                }
              }
            );
            //构建fileMd5参数,上传分块时带上fileMd5
            this.uploader.options.formData.fileMd5 = this.fileMd5;
            this.uploader.options.formData.chunk = block.chunk;
            return deferred.promise();
          }.bind(this),

后端

    // 检查分块是否存在
    public CheckChunkResult checkChunk(String fileMd5, Integer chunk, Integer chunkSize) {
        // 检查分块文件是否存在
        // 得到分块所在路径
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        // 分块所在路径+分块的索引可定位具体分块
        if (new File(chunkFileFolderPath + chunk).exists()) {
            return new CheckChunkResult(MediaCode.CHUNK_FILE_EXIST_CHECK, true);
        }
        return new CheckChunkResult(CommonCode.SUCCESS, false);
    }

上传文件

我们选择分块上传文件,chunked设置为true
在这里插入图片描述

上传文件使用的是IOUtils.copy(in, out);方法

    // 上传分块
    public ResponseResult uploadChunk(MultipartFile file, String fileMd5, Integer chunk) {
        // 检查分块目录是否存在
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        if (!chunkFileFolder.exists()) {
            chunkFileFolder.mkdirs();
        }

        // 上传文件输入流
        InputStream inputStream = null;
        FileOutputStream outputStream = null;
        try {
            inputStream = file.getInputStream();
            outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunk));
            IOUtils.copy(inputStream, outputStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return ResponseResult.SUCCESS();
    }


分块的合并与校验

全部分块上传后,我们就要进行合并分块的工作、校验、与入库。

合并分块:使用RandomAccessFile类进行读写操作,将排序好的分块列表遍历写入到合并文件。

校验:就是将前端WebUploader所生成的MD5与后端合并好的文件所生成的MD5进行比对。

    // 合并分块、校验MD5、数据入库
    public ResponseResult mergeChunks(String fileMd5, String fileName, Long fileSize, String mimetype, String fileExt) {
        // 1. 合并分块
        String chunkFileFolderPath = this.getChunkFileFolderPath(fileMd5);
        File chunkFileFolder = new File(chunkFileFolderPath);
        File[] files = chunkFileFolder.listFiles();

        String filePath = this.getFilePath(fileMd5, fileExt);
        File mergeFile = new File(filePath);
        List<File> fileList = Arrays.asList(files);

        // 合并
        mergeFile = this.mergeFile(fileList, mergeFile);
        if (mergeFile == null) {
            ExceptionCast.cast(MediaCode.MERGE_FILE_FAIL);
        }

        // 2. 校验文件MD5是否与前端传入一致
        boolean checkResult = this.checkFileMd5(mergeFile, fileMd5);
        // 校验失败
        if (!checkResult) {
            ExceptionCast.cast(MediaCode.MERGE_FILE_CHECKFAIL);
        }

        // 3. 文件信息写入MongoDB
        MediaFile mediaFile = new MediaFile();
        mediaFile.setFileId(fileMd5);
        mediaFile.setFileOriginalName(fileName);
        mediaFile.setFileName(fileMd5 + "." + fileExt);
        // 文件相对路径
        String relativeFilePath = fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2)+ "/" + fileMd5 + "/";
        mediaFile.setFilePath(relativeFilePath);
        mediaFile.setFileSize(fileSize);
        mediaFile.setUploadTime(new Date());
        mediaFile.setMimeType(mimetype);
        mediaFile.setFileType(fileExt);
        // 上传成功状态码
        mediaFile.setFileStatus(UPLOADED);
        mediaFileRepository.save(mediaFile);

        return ResponseResult.SUCCESS();
    }

    /**
     * 合并文件
     * @param chunkFileList
     * @param mergeFile
     * @return
     */
    private File mergeFile(List<File> chunkFileList, File mergeFile) {
        try {
            // 有删 无创建
            if (mergeFile.exists()) {
                mergeFile.delete();
            } else {
                mergeFile.createNewFile();
            }
            // 排序
            Collections.sort(chunkFileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    if (Integer.parseInt(o1.getName()) > Integer.parseInt(o2.getName())) {
                        return 1;
                    }
                    return -1;
                }
            });

            byte[] b = new byte[1024];
            RandomAccessFile writeFile = new RandomAccessFile(mergeFile, "rw");
            for (File chunkFile : chunkFileList) {
                RandomAccessFile readFile = new RandomAccessFile(chunkFile, "r");
                int len = -1;
                while ((len = readFile.read(b)) != -1) {
                    writeFile.write(b, 0, len);
                }
                readFile.close();
            }
            writeFile.close();
            return mergeFile;

        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 校验文件MD5
     * @param mergeFile
     * @param md5
     * @return
     */
    private boolean checkFileMd5(File mergeFile, String md5) {
        try {
            // 得到文件MD5
            FileInputStream inputStream = new FileInputStream(mergeFile);
            String md5Hex = DigestUtils.md5Hex(inputStream);

            if (StringUtils.equalsIgnoreCase(md5, md5Hex)) {
                return true;
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }


效果测试

在上传过程中,关闭上传页面(中断上传)再次上传,在checkchunk请求时发现分块都已存在,继续checkchunk直到分块不存在再进行uploadchunk。
在这里插入图片描述
分块全部上传完会请求合并,成功后提示上传成功。
在这里插入图片描述
磁盘
在这里插入图片描述
数据库

主键为文件的MD5值
在这里插入图片描述
而当上传相同文件的时候,因为在register的准备工作中做过了判断,因为会上传不成功。
在这里插入图片描述


BUG

有一点比较迷的是,其他文件都能上传成功,而我的英雄学院这集动画上传后在磁盘中分块和合并视频都存在,而且也能正常播放,但是后端生成的md5码就是与前端的不同,导致返回上传不成功的状态码。
在这里插入图片描述
然后我找了个在线生成文件MD5码的网站:
在这里插入图片描述
和后端生成的一样,那说明问题出在前端(没错本人前端cv工程师)。

然后我再看了下前端的生成md5的方法:
md5File(file, 0, 100*1024*1024)

好吧这里做了文件大小的限制,而动漫视频么,挺大的,把它设置大点后,生成的md5就与后端相同了,视频上传成功。

md5File( file[, start[, end]] ) ⇒ promise

           md5File: function( file, start, end ) {
                var md5 = new Md5(),
                    deferred = Base.Deferred(),
                    blob = (file instanceof Blob) ? file :
                        this.request( 'get-file', file ).source;

                md5.on( 'progress load', function( e ) {
                    e = e || {};
                    deferred.notify( e.total ? e.loaded / e.total : 1 );
                });

                md5.on( 'complete', function() {
                    deferred.resolve( md5.getResult() );
                });

                md5.on( 'error', function( reason ) {
                    deferred.reject( reason );
                });

                if ( arguments.length > 1 ) {
                    start = start || 0;
                    end = end || 0;
                    start < 0 && (start = blob.size + start);
                    end < 0 && (end = blob.size + end);
                    end = Math.min( end, blob.size );
                    blob = blob.slice( start, end );
                }

                md5.loadFromBlob( blob );

                return deferred.promise();
            }
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值