在浏览器进行大文件分片上传(java服务端实现)

最近在做web网盘的系统,网盘最基本的功能便是文件上传,但是文件上传当遇到大文件的时候,在web端按传统方式上传简直是灾难,所以大文件上传可以采用分片上传的办法。

其主要思路是:

  • 1.大文件上传时进行分片;
  • 2.分片上传;
  • 3.对分片文件进行合并。

思路比较清晰简单,但一些问题在于:

  • 1.大文件如何进行分片?
  • 2.分片如何进行记录和存储?
  • 3.如何校验每个分片文件的唯一性和顺序性?4.如何合并文件?

对于大文件如何分片,这个主要是在前端进行解决,在这里推荐大家用百度的WebUploader来实现前端所需。
对于对分片之后的文件进行存储的问题,我采用了临时文件存储的办法,临时文件存储着每个分块对应字节位的状态。

对于分片文件的区分,这里可以采用MD5码的方式(不清楚MD5码的可以先查一下),MD5码简单理解就像每个文件的身份证一样,每个不同的文件都有自己唯一的MD5码。
对于合并文件的时候,前端在对文件分片之后,在请求服务端合并的时候,请求中要带上分片序号和大小,服务器按照请求数据中给的分片序号和每片分块大小算出开始位置,与读取到的文件片段数据,写入文件即可。这里合并后的文件会存储俩个路径,一个是当前网盘目录下的路径,一个是真实的永久路径(目的是为了实现秒传的功能)。

前端分片的代码就不贴了,主要用的百度的WebUploader。

这里主要贴一些服务端的主要的代码

文件上传

/**
     * 上传文件
     *
     * @param file             文件
     * @param wholeMd5         文件整体md5码
     * @param name             文件名
     * @param type             文件类型
     * @param lastModifiedDate 上传时间
     * @param size             文件大小
     * @param chunks           文件分块数
     * @param chunk            正在执行的块
     */
    @ApiOperation(value = "文件上传", hidden = true)
    @IgnoreUserToken
    @ApiResponses({
            @ApiResponse(code = 500, response = RestError.class, message = "错误")
    })
    @PostMapping(value = "upload")
    public ResponseEntity<Integer> fileUpload(@ApiParam(name = "文件") @RequestPart MultipartFile file,
								              @ApiParam(name = "md5") @RequestParam String wholeMd5,
								              @ApiParam(name = "名称") @RequestParam String name,
								              @ApiParam(name = "类型") @RequestParam String type,
								              @ApiParam(name = "日期") @RequestParam Date lastModifiedDate,
								              @ApiParam(name = "大小") @RequestParam long size,
								              @ApiParam(name = "开始位置") @RequestParam long start,
								              @ApiParam(name = "结束位置") @RequestParam long end,
								              @ApiParam(name = "总分块数") @RequestParam(name = "chunks", defaultValue = "1") int chunks,
								              @ApiParam(name = "第几个分块,从0开始") @RequestParam(name = "chunk", defaultValue = "0") int chunk) {
		try {
			log.info("文件开始上传");
			this.fileServiceImpl.fileUpload(file.getInputStream(), wholeMd5, name, type, lastModifiedDate, size, chunks, chunk, start, end);
			return ResponseEntity.ok(1);
		} catch (Exception e) {
			return new ResponseEntity(RestError.IO_ERROR.setReason(e.getMessage()).toString(), HttpStatus.INTERNAL_SERVER_ERROR);
		}
	}
 @Override
    public boolean fileUpload(InputStream fileIS,
                              String wholeMd5,
                              String name, String type,
                              Date lastModifiedDate, long size,
                              int chunks,
                              int chunk,
                              long start,
                              long end) throws Exception {
        boolean result = false;
        try {
            File tempDirFile = new File(fileDir, TEMP_DIR);
            if (!tempDirFile.exists()) {
                tempDirFile.mkdirs();
            }
            // 块目录文件夹
            File wholeMd5FileDirectory = new File(tempDirFile.getAbsolutePath(), wholeMd5);
            if (!wholeMd5FileDirectory.exists()) {
                wholeMd5FileDirectory.mkdirs();
            }
            // 块文件
            File chunkFile = new File(wholeMd5FileDirectory.getAbsolutePath(), chunk + FILE_SEPARATOR + chunks + FILE_EXT);
            long chunkSize = end - start;
            if (!chunkFile.exists() || chunkFile.length() != chunkSize) {
                // 创建新的块文件
                long startTime = System.currentTimeMillis();
                log.info("创建建分片{} - {} ", start, end);
                int length = StreamUtils.copy(fileIS, new FileOutputStream(chunkFile));
                long endTime = System.currentTimeMillis();
                log.info("分片上传耗时{}毫秒", (endTime - startTime));
                if (length == (end - start)) {
                    result = true;
                }
            }
 
        } catch (Exception e) {
            log.error("文件上传出错{}", e.getCause());
            e.printStackTrace();
            throw e;
        }
        return result;
    }

检查文件的MD5

    /**
     * 检查文件的md5
     *
     * @param md5      文件md5
     * @param fileSize 文件大小
     * @return
     */
    @ApiOperation(value = "检查文件的md5")
    @GetMapping(value = "checkFileMd5/{md5}/{fileSize}/{md5CheckLength}")
    @ApiResponses({
            @ApiResponse(code = 500, response = RestError.class, message = "错误")
    })
    public ResponseEntity<Integer> checkFileMd5(@ApiParam("文件md5码") @PathVariable String md5,
                                                @ApiParam("文件大小") @PathVariable long fileSize,
                                                @ApiParam("文件用来检查md5的长度") @PathVariable long md5CheckLength) {
        try {
            log.info("开始检验md5[{}],是否存在", md5);
            return ResponseEntity.ok(this.fileServiceImpl.checkFileMd5(md5, fileSize, md5CheckLength) ? 1 : 0);
        } catch (Exception e) {
            return new ResponseEntity(RestError.DATABASE_ERROR.setReason(e.getMessage()).toString(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
 @Override
    public boolean checkFileMd5(String md5, long fileSize, long md5CheckLength) {
        Optional<UploadFileInfo> uploadFileInfo = this.uploadFileDao.findByMd5AndSize(md5, fileSize);
        boolean isExist = false;
        if (uploadFileInfo.isPresent()) {
            File wholeFile = new File(this.fileDir, uploadFileInfo.get().getDfsPath());
            if (wholeFile.exists() && wholeFile.length() == fileSize && md5.equals(FileUtils.md5(wholeFile, 0, md5CheckLength))) {
                isExist = true;
            }
        }
        log.info("{}的文件{}存在", md5, isExist ? "" : "不");
        return isExist;
    }

检查分片是否存在

/**
     * 检查分片是否存在
     *
     * @param md5
     * @param chunk
     * @param chunks
     * @param chunkStart
     * @param chunkEnd
     * @return
     */
    @ApiOperation(value = "检查分片是否存在")
    @ApiResponses({
            @ApiResponse(code = 500, response = RestError.class, message = "错误")
    })
    @GetMapping(value = "checkChunk/{md5}/{blockMd5}/{md5CheckLength}/{chunk}/{chunks}/{chunkStart}/{chunkEnd}")
    public ResponseEntity<Integer> checkChunk(@ApiParam("文件md5码") @PathVariable String md5,
                                              @ApiParam("分块文件md5码") @PathVariable String blockMd5,
                                              @ApiParam("用来检测分块文件md5码的长度") @PathVariable long md5CheckLength,
                                              @ApiParam("第几个分块,从0开始") @PathVariable int chunk,
                                              @ApiParam("总分块数") @PathVariable int chunks,
                                              @ApiParam("分块开始位于的文件位置") @PathVariable long chunkStart,
                                              @ApiParam("分块结束位于的文件位置") @PathVariable long chunkEnd) {
        try {
            log.info("开始检验分片[{}]-[{}]的md5[{}],是否存在", chunk, chunks, blockMd5);
            return ResponseEntity.ok(this.fileServiceImpl.checkChunk(md5, blockMd5, md5CheckLength, chunk, chunks, chunkStart, chunkEnd) ? 1 : 0);
        } catch (Exception e) {
            return new ResponseEntity(RestError.DATABASE_ERROR.setReason(e.getMessage()).toString(), HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
 @Override
    public boolean checkChunk(String md5, String blockMd5, long md5CheckLength, int chunk, int chunks, long chunkStart, long chunkEnd) {
        boolean isExist = false;
        File chunkFile = new File(fileDir, TEMP_DIR + File.separator + md5 + File.separator + chunk + FILE_SEPARATOR + chunks + FILE_EXT);
        if (chunkFile.exists() && chunkFile.length() == (chunkEnd - chunkStart)) {
            String calBlockMd5 = FileUtils.md5(chunkFile, 0, md5CheckLength);
            if (blockMd5.equals(calBlockMd5)) {
                isExist = true;
            }
        }
        log.info("{}的{}-{}分块{}存在", md5, chunk, chunks, isExist ? "" : "不");
        return isExist;
    }

合并文件

 /**
     * 合并文件
     *
     * @param fileInfo
     * @return
     */
    @ApiOperation(value = "合并文件", notes = "把分片上传的数据合并到一个文件")
    @ApiResponses({
            @ApiResponse(code = 500, response = RestError.class, message = "错误")
    })
    @PostMapping(value = "mergeChunks")
    public ResponseEntity<Integer> mergeChunks(@Validated @RequestBody FileInfo fileInfo, BindingResult bindingResult) {
        log.info("开始合并文件");
        if (bindingResult.hasErrors()) {
            log.error("错误的参数请求");
            return new ResponseEntity("错误的参数请求", HttpStatus.BAD_REQUEST);
        } else {
            try {
                DataEntity dataEntity = this.fileServiceImpl.mergeChunks(fileInfo);
                log.info("合并文件完成, 保存的dataEntityId为:{}", dataEntity != null ? dataEntity.getId() : null);
                return ResponseEntity.ok(dataEntity != null ? 1 : 0);
            } catch (FileMargeException e) {
                log.error(e.getMessage(), e);
                return new ResponseEntity(RestError.FILE_MARGE_ERROR.setReason(e.getMessage()).toString(), HttpStatus.INTERNAL_SERVER_ERROR);
            } catch (FileNotAllException e) {
                log.error(e.getMessage(), e);
                return new ResponseEntity(RestError.FILE_NOTALL_ERROR.setReason(e.getMessage()).toString(), HttpStatus.INTERNAL_SERVER_ERROR);
            } catch (IOException e) {
                log.error(e.getMessage(), e);
                return new ResponseEntity(RestError.IO_ERROR.setReason(e.getMessage()).toString(), HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
    }

​ 关于秒传功能,其实原理就是检验文件MD5,在一个文件上传前先获取文件内容MD5值或者部分取值MD5,然后在查找自己的记录是否已存在相同的MD5,如果存在就直接从服务器真实路径取,而不需要重新进行分片上传了,从而达到秒传的效果。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值