springboot 后端大文件分片上传(断点上传)
目录简介1. 数据库表创建2. 编写文件记录标识接口3. 编写分片是否存在判断接口4. 编写文件分片上传接口5. 编写文件分片合并接口简介什么是分片上传?分片上传就是把一个大的文件分成N个部分然后一部分一部分的进行上传。分片上传有什么好处分片上传可以实现断网、关浏览器、传输错误等情况发生后再次进行此文件的上传的时候已上传的部分无需继续上传减少上传文件的流量消耗与用户的等待时间。后端如何实现分片上传后端传统上传直接开发一个接口接受文件即可但是分片上传如何实现呢按照分片的解读理解咱们可以将上传接口进行拆分①. 首先咱们需要一个接口来记录标识一个文件这样才能区分不同文件②. 然后咱们还需要一个接口用来判断某个分片是否已经上传完成③. 再然后还需要一个接口用来进行分片文件的上传其实就是一个简单的上传功能将文件分片存放并且记录到文件分片上传记录中④. 最后还需要一个最重要的接口那当然就是分片的合并接口了调用此接口按照顺序将某个文件的所有分片合并成真实的对应的文件。那么废话不多说下面咱们就开始编码有问题可留言评论断点上传不是断点续传断点续传是特指下载这又是另外的知识了注ResponseVo为自定义统一返回格式自行定义接口中用到jackjson包;接口中用到CommonException为自定义异常自行定义或其他方式处理接口中用到FileExistsResponse为判断文件分片是否存在的返回数据结构自行定义接口中用到的filemapper等数据库层的内容自行实现数据库涉及到一张表file_uploadFileUpload为文件上传表file_upload的实体1. 数据库表创建create table file_upload ( id NUMBER not null, index NUMBER, parent NUMBER, name VARCHAR2(100 char) not null, path VARCHAR2(255 char), md5 VARCHAR2(200 char) not null, size NUMBER not null constraint PK_FILE_UPLOAD primary key (id) );2. 编写文件记录标识接口此接口每个文件调用一次调用完成后需要返回文件记录的信息主要是返回ID以便前端拿到后作为该文件所有分片的parent。接口入参public class FileNewRequest { private String index; private String parent; private String name; private String path; private String md5; private String size; public String getIndex() { return index; } public void setIndex(String index) { this.index index; } public String getParent() { return parent; } public void setParent(String parent) { this.parent parent; } public String getName() { return name; } public void setName(String name) { this.name name; } public String getPath() { return path; } public void setPath(String path) { this.path path; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public String getSize() { return size; } public void setSize(String size) { this.size size; } }接口/** * 新创建一个文件标识记录 * author kevin * param request : * return com.liu.services.system.entity.FileUpload * date 2021/1/8 */ ApiOperation(value 文件标识记录, notes /file/new) PostMapping(/file/new) public ResponseVo uploadFile(Valid RequestBody FileNewRequest request) { String fileName request.getName(); String md5 request.getMd5(); try { //写入文件标识记录 FileUpload file (FileUpload) JSONObject.toBean(JSONObject.fromObject(request), FileUpload.class); //如果文件标识记录不存在则插入;如果存在则更新文件大小 FileUpload fileUpload fileMapper.findByMd5AndName(md5, fileName); if (ObjectUtils.isNotEmpty(fileUpload)) { fileMapper.updateSize(file); } else { fileMapper.insert(file); } fileUpload fileMapper.findByMd5AndName(md5, fileName); return new ResponseVo.Builder().ok().data(fileUpload).build(); }catch (Exception e){ return new ResponseVo.Builder().error().message(String.format(文件%1$s 上传错误, fileName)).build(); } }3. 编写分片是否存在判断接口此接口前端每个分片处理都需要调用判断跟第4点中的接口配合使用此处parent为第2点中接口返回的ID接口入参public class FileExistsRequest { private Long parent; private String md5; private Long size; public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public Long getSize() { return size; } public void setSize(Long size) { this.size size; } }接口/** * 文件分片上传判断文件记录或者文件分片是否已经存在 * * param request : * return com.liu.base.ResponseVo * author kevin * date 2021/1/9 */ PostMapping(/file/exists) public ResponseVo fileExists(Valid RequestBody FileExistsRequest request) { FileUpload file; if(null ! request.getParent()){ file fileMapper.findByParentAndMd5(String.valueOf(request.getParent()), request.getMd5()); }else{ ListFileUpload files fileMapper.findByMd5(request.getMd5()); //查找父文件ID不为空的记录即文件分片记录 files files.stream().filter(item - item.getParent() ! null).collect(Collectors.toList()); int count files.size(); if(count 1){ throw new CommonException(找到了不止一个分片/文件记录); }else if (count 1){ file null; }else { file files.get(0); } } //文件标识或文件片不存在 if(file null) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); } //无父文件ID则表示记录为文件标识记录判断文件是否存在 if(null file.getParent() || 0 file.getParent()){ ListFileUpload patchs fileMapper.findByParent(file.getId()); Long total patchs.stream().mapToLong(FileUpload::getSize).sum(); if(total.equals(request.getSize())) { return new ResponseVo.Builder().ok().data(FileExistsResponse.exists(file.getId())).build(); }else if(total 0) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); }else { return new ResponseVo.Builder().ok().data( FileExistsResponse.partExistent(file.getId(), fileMapper.findPatchIndexByParent(file.getId())) ).build(); } } //父文件ID存在需要判断的内容为文件片 //文件片存在且大小为正确的大小表示文件片已存在 if(file.getFileSize().equals(request.getFileSize())) { return new ResponseVo.Builder().ok().data(FileExistsResponse.exists(file.getId())).build(); } //文件片大小为0表示文件片还未上传 if(file.getFileSize() 0) { return new ResponseVo.Builder().ok().data(FileExistsResponse.nonExistent()).build(); } //其他文件片大小不为0又不等于需要上传的文件片大小表示文件片存在部分 return new ResponseVo.Builder().ok().data( FileExistsResponse.partExistent(file.getId(), fileMapper.findPatchIndexByParent(file.getId())) ).build(); }4. 编写文件分片上传接口此接口在前端调用文件分片是否存在判断分片不存在或只存在部分后调用此处parent为第2点中接口返回的ID接口入参public class FilePatchUploadRequest { private String name; private Long index; private Long parent; private String md5; private Long size; public String getName() { return name; } public void setName(String name) { this.name name; } public Long getIndex() { return index; } public void setIndex(Long index) { this.index index; } public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 md5; } public Long getSize() { return size; } public void setSize(Long size) { this.size size; } }接口/** * 文件分片上传 * * param request : * param patch : * return com.liu.base.ResponseVo * author kevin * date 2021/1/7 */ PostMapping(/file/patch/upload) public ResponseVo filePatchUpload(Valid FilePatchUploadRequest request, RequestParam(patch) MultipartFile patch){ Long index request.getIndex(); //判断分片文件是否存在 if(patch.isEmpty()){ return new ResponseVo.Builder().error().message(第:%1$s 个分片未获取到分片文件).build(); } Long parent request.getParent(); Long size request.getSize(); String md5 request.getMd5(); String name request.getName(); //开始分片保存 FileUpload file fileMapper.findByParentAndMd5(String.valueOf(parent), md5); try { //判断数据库如果分片不存在或者分片大小与上传的分片的大小不一致则上传保存分片 if (file null || !file.getFileSize().equals(size)) { Optional.ofNullable(file).ifPresent(e - fileMapper.deleteByPk(e.getId())); //保存分片文件到服务器构建文件分片记录信息 FileUpload fileUpload new FileUpload(index, parent, name, saveFile(patch, size, d:/), md5, size); return new ResponseVo.Builder().ok().build(); } }catch (IOException e){ log.error(SystemConst.PATCH_UPLOAD_FILE_ERROR_LOG, name, index, e); throw new CommonException(ResponseState.REQUEST_ERROR.getCode(), String.format(SystemConst.UPLOAD_FILE_PATCH_ERROR, name, index)); } return new ResponseVo.Builder().ok().build() ; } /** * 保存文件块(此方法可提取为一个工具类中的工具方法) * author kevin * param source : 文件块 * param size : 文件块大小 * return java.lang.String * date 2021/1/7 */ private String saveFile(MultipartFile source, Long size, String path) throws IOException { String now LocalDateTime.now().format(DateTimeFormatter.ofPattern(SystemConst.DATE_ONLY_FORMATTER)); if(null source || source.isEmpty()) { throw new CommonException(未获取到需要保存的分片文件); } if(source.getSize() ! size) { throw new CommonException(分片文件大小与传入的分片大小不一致); } path path File.separator now; File file new File(path); if(!file.exists() !file.mkdirs()) { throw new CommonException(创建文件夹失败); } //根据文件名获取文件类型 String fileType (null fileName || !fileName.contains(.)) ? unknown : fileName.substring(fileName.lastIndexOf(.)); //保存文件块到磁盘 while(true) { String saveFileName UUID.randomUUID() fileType.toLowerCase() .tmp; File saveFilePath new File(path, saveFileName); //磁盘上块文件名如果已经存在则重新生成文件名 if(saveFilePath.exists()) { continue; } source.transferTo(saveFilePath); return saveFilePath.getAbsolutePath(); } }5. 编写文件分片合并接口此接口在前端确认所有分片已完成上传后调用。此处parent为第2点中接口返回的ID接口入参public class FileMergeRequest { private Long parent; private Long fileSize; public Long getParent() { return parent; } public void setParent(Long parent) { this.parent parent; } public Long getFileSize() { return fileSize; } public void setFileSize(Long fileSize) { this.fileSize fileSize; } }接口/** * 文件分片合并 * author kevin * param request : * return com.liu.base.ResponseVo * date 2021/1/8 */ ApiOperation(value 文件分片合并, notes /file/merge) PostMapping(/file/merge) public ResponseVo filePatchMerge(Valid RequestBody FileMergeRequest request) { Long parent request.getParent(); Long size request.getFileSize(); //查询当前文件 FileUpload fileInfo fileMapper.findByPk(parent); String fileName null fileInfo ? : fileInfo.getName(); String md5 null fileInfo ? : fileInfo.getMd5(); try { //查询当前文件所有分块 ListFileUpload patchs fileMapper.findByParentPatchIndex(parent); Long total patchs.stream().mapToLong(FileUpload::getFileSize).sum(); if (fileInfo null || CollectionUtils.isEmpty(patchs) || !total.equals(size)) { fileMapper.deleteByParent(parent); log.error(String.format(文件合并失败大小不正确); return new ResponseVo.Builder().error().message(文件合并失败大小不正确).build(); } //合并文件 String path mergeFile(fileName, patchs.stream().map(FileUpload::getPath) .collect(Collectors.toList()), filePath, userName); fileMapper.updateByIdSetPathAndSize(String.valueOf(parent), path, String.valueOf(total)); fileMapper.deleteByParent(parent); }catch (Exception e){ fileMapper.deleteByParent(parent); e.printStackTrace(); return new ResponseVo.Builder().error().message(文件合并失败e.getMessage()).build(); } return new ResponseVo.Builder().ok().build(); } /** * 合并文件块(此方法可提取到工具类) * author kevin * param fileName : 文件名称 * param filePaths : 文件分片路径 * param path : 文件存放路径 * param directory : 文件存放目录 * return java.lang.String * date 2021/1/8 */ private String mergeFile(String fileName, ListString filePaths, String path, String directory) throws IOException { String now LocalDateTime.now().format(DateTimeFormatter.ofPattern(SystemConst.DATE_ONLY_FORMATTER)); path path File.separator (StringUtils.isEmpty(directory) ? : (directory File.separator)) now; File file new File(path); if(!file.exists() !file.mkdirs()) { throw new CommonException(创建文件夹失败); } //生成文件名 File saveFilePath new File(path, fileName); while (saveFilePath.exists()){ //生成文件名 Files.delete(saveFilePath.toPath()); } //创建文件输出流 try (FileOutputStream fos new FileOutputStream(saveFilePath)){ FileChannel out fos.getChannel(); //循环每一个文件块对应的文件 for (String filename : filePaths) { File patch new File(filename); //将每一个文件块的内容都追加到新文件中 try (FileChannel in new FileInputStream(patch).getChannel()) { in.transferTo(0, in.size(), out); } //追加完成后删除文件块 Files.delete(patch.toPath()); } out.close(); } return saveFilePath.getAbsolutePath(); }

相关新闻

最新新闻

日新闻

周新闻

月新闻