小言_互联网的博客

教程 | 叮咚!答应你们的文件上传教程,到货了!

511人阅读  评论(0)

Hi! 我是小小,今天是本周的第五篇,小小又懒惰了,没有半夜更新,没办法,一大早起来开始更新。

前言

文件上传是网络开发中常见的环节,对于文件上传,有以下几种的上传方式,分别是秒传,断点续传,分片上传

秒传

什么是秒传

秒传就是服务器会先做MD5校验,根据校验的结果,如果服务器上有相同的文件,则会直接给出这个文件在服务器上的地址,如果没有则会按照正常的方式进行文件的上传。

实现核心逻辑

利用redis的set方法保存文件的上传状态,其中key为文件上传的md5,value为上传完成的标志位。当标志位为true的时候,上传完成,如果有相同的文件上传,进入秒传逻辑,如果标志位为false,那么文件还没有上传完成,此时需要再次调用set方法, 保存文件记录的路径,其中key为文件上传的md5加上一个前缀,value为块号文件的记录的路径

分片上传

什么是分片上传

分片上传,就是把文件进行分割开来进行上传

上传场景

大文件上传 网络环境不好的时候

断点续传

什么是断点续传

断点续传就是文件在下载或者上传的时候,如果临时中断相关操作,那么过一段时间重新开始相关操作,那么相关的操作仍旧可以继续延续。

应用场景

可以使用分片上传的场景,都可以使用断点续传

核心逻辑

需要记录文件上传的进度,在之后继续上传的时候,从原先的进度重新开始。为了避免客户端数据删除,导致重新上传的问题,服务器端也需要进行相关的记录。

实现步骤

方案一,常规步骤

把需要上传的文件,按照一定的分割规则,分割为相同大小的数据块 初始化一个分片上传任务,返回本次分片上传的唯一标识 按照一定的方式,发送相关的数据块。发送完成以后,服务端根据数据是否完整,重新进行相关的整合。

方案二:本文实现的步骤

前端,需要根据固定大小对文件进行分片,请求后端要带上分片序号和大小 服务器创建一个conf文件进行记录分块的位置,conf文件长度为总分片的数量,每上传一个分片,向conf文件写入一个127,那么美上传的就是0,已经上传的就是127. 服务器搜索相关的分片大小,算出初始位置,开始写入文件。

代码实现

  1. 前端采用百度提供的webuploader的插件,进行分片。具体链接如下所示:http://fex.baidu.com/webuploader/getting-started.html

  2. 后端用两种方式实现文件写入,一种是用RandomAccessFile,如果对RandomAccessFile不熟悉的朋友,可以查看如下链接: https://blog.csdn.net/dimudan2015/article/details/81910690

后端写入的核心代码

RandomAccessFile实现方式


   
  1. @UploadMode(mode = UploadModeEnum.RANDOM_ACCESS)
  2. @Slf4j
  3. public class RandomAccessUploadStrategy extends SliceUploadTemplate {
  4. @Autowired
  5. private FilePathUtil filePathUtil;
  6. @Value( "${upload.chunkSize}")
  7. private long defaultChunkSize;
  8. @Override
  9. public boolean upload(FileUploadRequestDTO param) {
  10. RandomAccessFile accessTmpFile = null;
  11. try {
  12. String uploadDirPath = filePathUtil.getPath(param);
  13. File tmpFile = super.createTmpFile(param);
  14. accessTmpFile = new RandomAccessFile(tmpFile, "rw");
  15. //这个必须与前端设定的值一致
  16. long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
  17. : param.getChunkSize();
  18. long offset = chunkSize * param.getChunk();
  19. //定位到该分片的偏移量
  20. accessTmpFile.seek(offset);
  21. //写入该分片数据
  22. accessTmpFile.write(param.getFile().getBytes());
  23. boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
  24. return isOk;
  25. } catch (IOException e) {
  26. log.error(e.getMessage(), e);
  27. } finally {
  28. FileUtil. close(accessTmpFile);
  29. }
  30. return false;
  31. }
  32. }

MappedByteBuffer实现方式


   
  1. @UploadMode(mode = UploadModeEnum.MAPPED_BYTEBUFFER)
  2. @Slf4j
  3. public class MappedByteBufferUploadStrategy extends SliceUploadTemplate {
  4. @Autowired
  5. private FilePathUtil filePathUtil;
  6. @Value( "${upload.chunkSize}")
  7. private long defaultChunkSize;
  8. @Override
  9. public boolean upload(FileUploadRequestDTO param) {
  10. RandomAccessFile tempRaf = null;
  11. FileChannel fileChannel = null;
  12. MappedByteBuffer mappedByteBuffer = null;
  13. try {
  14. String uploadDirPath = filePathUtil.getPath(param);
  15. File tmpFile = super.createTmpFile(param);
  16. tempRaf = new RandomAccessFile(tmpFile, "rw");
  17. fileChannel = tempRaf.getChannel();
  18. long chunkSize = Objects.isNull(param.getChunkSize()) ? defaultChunkSize * 1024 * 1024
  19. : param.getChunkSize();
  20. //写入该分片数据
  21. long offset = chunkSize * param.getChunk();
  22. byte[] fileData = param.getFile().getBytes();
  23. mappedByteBuffer = fileChannel
  24. . map(FileChannel.MapMode.READ_WRITE, offset, fileData.length);
  25. mappedByteBuffer.put(fileData);
  26. boolean isOk = super.checkAndSetUploadProgress(param, uploadDirPath);
  27. return isOk;
  28. } catch (IOException e) {
  29. log.error(e.getMessage(), e);
  30. } finally {
  31. FileUtil.freedMappedByteBuffer(mappedByteBuffer);
  32. FileUtil. close(fileChannel);
  33. FileUtil. close(tempRaf);
  34. }
  35. return false;
  36. }
  37. }

文件操作核心模板类代码


   
  1. @Slf4j
  2. public abstract class SliceUploadTemplate implements SliceUploadStrategy {
  3. public abstract boolean upload(FileUploadRequestDTO param);
  4. protected File createTmpFile(FileUploadRequestDTO param) {
  5. FilePathUtil filePathUtil = SpringContextHolder.getBean(FilePathUtil.class);
  6. param.setPath(FileUtil.withoutHeadAndTailDiagonal(param.getPath()));
  7. String fileName = param.getFile().getOriginalFilename();
  8. String uploadDirPath = filePathUtil.getPath(param);
  9. String tempFileName = fileName + "_tmp";
  10. File tmpDir = new File(uploadDirPath);
  11. File tmpFile = new File(uploadDirPath, tempFileName);
  12. if (!tmpDir.exists()) {
  13. tmpDir.mkdirs();
  14. }
  15. return tmpFile;
  16. }
  17. @Override
  18. public FileUploadDTO sliceUpload(FileUploadRequestDTO param) {
  19. boolean isOk = this.upload(param);
  20. if (isOk) {
  21. File tmpFile = this.createTmpFile(param);
  22. FileUploadDTO fileUploadDTO = this.saveAndFileUploadDTO(param.getFile().getOriginalFilename(), tmpFile);
  23. return fileUploadDTO;
  24. }
  25. String md5 = FileMD5Util.getFileMD5(param.getFile());
  26. Map<Integer, String> map = new HashMap<>();
  27. map.put(param.getChunk(), md5);
  28. return FileUploadDTO.builder().chunkMd5Info( map).build();
  29. }
  30. /**
  31. * 检查并修改文件上传进度
  32. */
  33. public boolean checkAndSetUploadProgress(FileUploadRequestDTO param, String uploadDirPath) {
  34. String fileName = param.getFile().getOriginalFilename();
  35. File confFile = new File(uploadDirPath, fileName + ".conf");
  36. byte isComplete = 0;
  37. RandomAccessFile accessConfFile = null;
  38. try {
  39. accessConfFile = new RandomAccessFile(confFile, "rw");
  40. //把该分段标记为 true 表示完成
  41. System.out. println( "set part " + param.getChunk() + " complete");
  42. //创建conf文件文件长度为总分片数,每上传一个分块即向conf文件中写入一个127,那么没上传的位置就是默认0,已上传的就是Byte.MAX_VALUE 127
  43. accessConfFile.setLength(param.getChunks());
  44. accessConfFile.seek(param.getChunk());
  45. accessConfFile.write(Byte.MAX_VALUE);
  46. //completeList 检查是否全部完成,如果数组里是否全部都是127(全部分片都成功上传)
  47. byte[] completeList = FileUtils.readFileToByteArray(confFile);
  48. isComplete = Byte.MAX_VALUE;
  49. for ( int i = 0; i < completeList.length && isComplete == Byte.MAX_VALUE; i++) {
  50. //与运算, 如果有部分没有完成则 isComplete 不是 Byte.MAX_VALUE
  51. isComplete = ( byte) (isComplete & completeList[i]);
  52. System.out. println( "check part " + i + " complete?:" + completeList[i]);
  53. }
  54. } catch (IOException e) {
  55. log.error(e.getMessage(), e);
  56. } finally {
  57. FileUtil. close(accessConfFile);
  58. }
  59. boolean isOk = setUploadProgress2Redis(param, uploadDirPath, fileName, confFile, isComplete);
  60. return isOk;
  61. }
  62. /**
  63. * 把上传进度信息存进redis
  64. */
  65. private boolean setUploadProgress2Redis(FileUploadRequestDTO param, String uploadDirPath,
  66. String fileName, File confFile, byte isComplete) {
  67. RedisUtil redisUtil = SpringContextHolder.getBean(RedisUtil.class);
  68. if (isComplete == Byte.MAX_VALUE) {
  69. redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "true");
  70. redisUtil.del(FileConstant.FILE_MD5_KEY + param.getMd5());
  71. confFile. delete();
  72. return true;
  73. } else {
  74. if (!redisUtil.hHasKey(FileConstant.FILE_UPLOAD_STATUS, param.getMd5())) {
  75. redisUtil.hset(FileConstant.FILE_UPLOAD_STATUS, param.getMd5(), "false");
  76. redisUtil.set(FileConstant.FILE_MD5_KEY + param.getMd5(),
  77. uploadDirPath + FileConstant.FILE_SEPARATORCHAR + fileName + ".conf");
  78. }
  79. return false;
  80. }
  81. }
  82. /**
  83. * 保存文件操作
  84. */
  85. public FileUploadDTO saveAndFileUploadDTO(String fileName, File tmpFile) {
  86. FileUploadDTO fileUploadDTO = null;
  87. try {
  88. fileUploadDTO = renameFile(tmpFile, fileName);
  89. if (fileUploadDTO.isUploadComplete()) {
  90. System.out
  91. . println( "upload complete !!" + fileUploadDTO.isUploadComplete() + " name=" + fileName);
  92. //TODO 保存文件信息到数据库
  93. }
  94. } catch (Exception e) {
  95. log.error(e.getMessage(), e);
  96. } finally {
  97. }
  98. return fileUploadDTO;
  99. }
  100. /**
  101. * 文件重命名
  102. *
  103. * @param toBeRenamed 将要修改名字的文件
  104. * @param toFileNewName 新的名字
  105. */
  106. private FileUploadDTO renameFile(File toBeRenamed, String toFileNewName) {
  107. //检查要重命名的文件是否存在,是否是文件
  108. FileUploadDTO fileUploadDTO = new FileUploadDTO();
  109. if (!toBeRenamed.exists() || toBeRenamed.isDirectory()) {
  110. log.info( "File does not exist: {}", toBeRenamed.getName());
  111. fileUploadDTO.setUploadComplete( false);
  112. return fileUploadDTO;
  113. }
  114. String ext = FileUtil.getExtension(toFileNewName);
  115. String p = toBeRenamed.getParent();
  116. String filePath = p + FileConstant.FILE_SEPARATORCHAR + toFileNewName;
  117. File newFile = new File(filePath);
  118. //修改文件名
  119. boolean uploadFlag = toBeRenamed.renameTo(newFile);
  120. fileUploadDTO.setMtime(DateUtil.getCurrentTimeStamp());
  121. fileUploadDTO.setUploadComplete(uploadFlag);
  122. fileUploadDTO.setPath(filePath);
  123. fileUploadDTO.setSize(newFile.length());
  124. fileUploadDTO.setFileExt(ext);
  125. fileUploadDTO.setFileId(toFileNewName);
  126. return fileUploadDTO;
  127. }
  128. }

关于作者

我是小小,双鱼座的程序猿,我们下期再见~bye

END

「 往期文章 」

测试 | 测试:你会这些命令吗?

心酸 | Bean复制的几种框架对比,看完心酸

线程安全 | i++线程安全?真相是这样……

扫描二维码

获取更多精彩

小明菜市场

来源:网络(侵删)

图片来源:网络(侵删)

点个在看你最好看


转载:https://blog.csdn.net/melovemingming/article/details/112691012
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场