• SpringBoot大文件上传实现分片、断点续传


    大文件上传流程

    1. 客户端计算文件的哈希值,客户端将哈希值发送给服务端,服务端检查数据库或文件系统中是否已存在相同哈希值的文件,如果存在相同哈希值的文件,则返回秒传成功结果,如果不存在相同哈希值的文件,则进行普通的文件上传流程。
    2. 客户端将要上传的大文件切割成固定大小的切片,客户端将每个切片逐个上传给服务端。服务端接收到每个切片后,暂存或保存在临时位置,当所有切片上传完毕时,服务端将这些切片合并成完整的文件。
    3. 客户端记录上传进度,包括已上传的切片以及每个切片的上传状态。客户端将上传进度信息发送给服务端。如果客户端上传中断或失败,重新连接后发送上传进度信息给服务端。服务端根据上传进度信息确定断点位置,并继续上传剩余的切片。服务端接收切片后,将其暂存或保存在临时位置,当所有切片上传完毕时,服务端将这些切片合并成完整的文件。

    数据库部分

    创建分片数据表和文件数据表 

    1. create table if not exists file_chunk
    2. (
    3. id bigint unsigned auto_increment
    4. primary key,
    5. file_name varchar(255) null comment '文件名',
    6. chunk_number int null comment '当前分片,从1开始',
    7. chunk_size bigint null comment '分片大小',
    8. current_chunk_size bigint null comment '当前分片大小',
    9. total_size bigint null comment '文件总大小',
    10. total_chunk int null comment '总分片数',
    11. identifier varchar(128) null comment '文件校验码,md5',
    12. relative_path varchar(255) null comment '相对路径',
    13. create_by varchar(128) null comment '创建者',
    14. create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    15. update_by varchar(128) null comment '更新人',
    16. update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
    17. )
    18. comment '文件块存储';
    19. create table if not exists file_storage
    20. (
    21. id bigint auto_increment comment '主键'
    22. primary key,
    23. real_name varchar(128) null comment '文件真实姓名',
    24. file_name varchar(128) null comment '文件名',
    25. suffix varchar(32) null comment '文件后缀',
    26. file_path varchar(255) null comment '文件路径',
    27. file_type varchar(255) null comment '文件类型',
    28. size bigint null comment '文件大小',
    29. identifier varchar(128) null comment '检验码 md5',
    30. create_by varchar(128) null comment '创建者',
    31. create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
    32. update_by varchar(128) null comment '更新人',
    33. update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间'
    34. )
    35. comment '文件存储表';

    后端部分 

    引入依赖

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-thymeleaf</artifactId>
    4. </dependency>
    5. <dependency>
    6. <groupId>cn.hutool</groupId>
    7. <artifactId>hutool-all</artifactId>
    8. <version>5.7.22</version>
    9. </dependency>
    10. <dependency>
    11. <groupId>com.alibaba</groupId>
    12. <artifactId>fastjson</artifactId>
    13. <version>1.2.47</version>
    14. </dependency>
    15. <dependency>
    16. <groupId>org.springframework.boot</groupId>
    17. <artifactId>spring-boot-starter-data-redis</artifactId>
    18. </dependency>
    19. <!-- 数据库-->
    20. <dependency>
    21. <groupId>mysql</groupId>
    22. <artifactId>mysql-connector-java</artifactId>
    23. <version>${mysql.version}</version>
    24. </dependency>
    25. <dependency>
    26. <groupId>com.baomidou</groupId>
    27. <artifactId>mybatis-plus-boot-starter</artifactId>
    28. <version>3.5.1</version>
    29. </dependency>

     properties文件配置

    1. # 设置服务器上传最大文件大小为1g
    2. spring.servlet.multipart.max-file-size=1GB
    3. spring.servlet.multipart.max-request-size=1GB
    4. # 数据库连接
    5. spring.datasource.url=jdbc:mysql://127.0.0.1:3306/big-file?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
    6. spring.datasource.username=root
    7. spring.datasource.password=root123
    8. spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
    9. spring.datasource.hikari.pool-name=HikariCPDatasource
    10. spring.datasource.hikari.minimum-idle=5
    11. spring.datasource.hikari.idle-timeout=180000
    12. spring.datasource.hikari.maximum-pool-size=10
    13. spring.datasource.hikari.auto-commit=true
    14. spring.datasource.hikari.max-lifetime=1800000
    15. spring.datasource.hikari.connection-timeout=30000
    16. spring.datasource.hikari.connection-test-query=SELECT 1
    17. # redis连接
    18. spring.redis.host=xx.xx.xx.xx
    19. spring.redis.port=6379
    20. spring.redis.timeout=10s
    21. spring.redis.password=123
    22. # 配置thymeleaf模板
    23. spring.thymeleaf.cache=false
    24. spring.thymeleaf.prefix=classpath:/templates/
    25. spring.thymeleaf.suffix=.html
    26. # 文件存储位置
    27. file.path=D:\\tmp\\file
    28. # 分片大小设为20 * 1024 * 1024 kb
    29. file.chunk-size=20971520

    model

    返回给前端的vo判断是否已经上传过,是就秒传,不是就分片上传 

    1. import lombok.Data;
    2. import java.util.List;
    3. /**
    4. * 检验返回给前端的vo
    5. */
    6. @Data
    7. public class CheckResultVo {
    8. /**
    9. * 是否已上传
    10. */
    11. private Boolean uploaded;
    12. private String url;
    13. private List uploadedChunks;
    14. }

    接收前端发送过来的分片

    1. import lombok.Data;
    2. import org.springframework.web.multipart.MultipartFile;
    3. /**
    4. * 接收前端传过来的参数
    5. * 配合前端上传方法接收参数,参考官方文档
    6. * https://github.com/simple-uploader/Uploader/blob/develop/README_zh-CN.md#%E6%9C%8D%E5%8A%A1%E7%AB%AF%E5%A6%82%E4%BD%95%E6%8E%A5%E5%8F%97%E5%91%A2
    7. */
    8. @Data
    9. public class FileChunkDto {
    10. /**
    11. * 当前块的次序,第一个块是 1,注意不是从 0 开始的
    12. */
    13. private Integer chunkNumber;
    14. /**
    15. * 文件被分成块的总数。
    16. */
    17. private Integer totalChunks;
    18. /**
    19. * 分块大小,根据 totalSize 和这个值你就可以计算出总共的块数。注意最后一块的大小可能会比这个要大
    20. */
    21. private Long chunkSize;
    22. /**
    23. * 当前要上传块的大小,实际大小
    24. */
    25. private Long currentChunkSize;
    26. /**
    27. * 文件总大小
    28. */
    29. private Long totalSize;
    30. /**
    31. * 这个就是每个文件的唯一标示
    32. */
    33. private String identifier;
    34. /**
    35. * 文件名
    36. */
    37. private String filename;
    38. /**
    39. * 文件夹上传的时候文件的相对路径属性
    40. */
    41. private String relativePath;
    42. /**
    43. * 文件
    44. */
    45. private MultipartFile file;
    46. }

    数据库分片的实体类 

    1. import lombok.Data;
    2. import java.io.Serializable;
    3. import java.time.LocalDateTime;
    4. /**
    5. * 文件块存储(FileChunk)表实体类
    6. */
    7. @Data
    8. public class FileChunk implements Serializable {
    9. /**主键**/
    10. private Long id;
    11. /**文件名**/
    12. private String fileName;
    13. /**当前分片,从1开始**/
    14. private Integer chunkNumber;
    15. /**分片大小**/
    16. private Long chunkSize;
    17. /**当前分片大小**/
    18. private Long currentChunkSize;
    19. /**文件总大小**/
    20. private Long totalSize;
    21. /**总分片数**/
    22. private Integer totalChunk;
    23. /**文件标识 md5校验码**/
    24. private String identifier;
    25. /**相对路径**/
    26. private String relativePath;
    27. /**创建者**/
    28. private String createBy;
    29. /**创建时间**/
    30. private LocalDateTime createTime;
    31. /**更新人**/
    32. private String updateBy;
    33. /**更新时间**/
    34. private LocalDateTime updateTime;
    35. }

    数据库的文件的实体类 

    1. import lombok.Data;
    2. import java.io.Serializable;
    3. import java.time.LocalDateTime;
    4. /**
    5. * 文件存储表(FileStorage)表实体类
    6. */
    7. @Data
    8. public class FileStorage implements Serializable {
    9. /**主键**/
    10. private Long id;
    11. /**文件真实姓名**/
    12. private String realName;
    13. /**文件名**/
    14. private String fileName;
    15. /**文件后缀**/
    16. private String suffix;
    17. /**文件路径**/
    18. private String filePath;
    19. /**文件类型**/
    20. private String fileType;
    21. /**文件大小**/
    22. private Long size;
    23. /**检验码 md5**/
    24. private String identifier;
    25. /**创建者**/
    26. private String createBy;
    27. /**创建时间**/
    28. private LocalDateTime createTime;
    29. /**更新人**/
    30. private String updateBy;
    31. /**更新时间**/
    32. private LocalDateTime updateTime;
    33. }

    Mapper层

    操作分片的数据库的mapper 

    1. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    2. import com.example.demo.model.FileChunk;
    3. import org.apache.ibatis.annotations.Mapper;
    4. /**
    5. * 文件块存储(FileChunk)表数据库访问层
    6. */
    7. @Mapper
    8. public interface FileChunkMapper extends BaseMapper {
    9. }

    操作文件的数据库的mapper

    1. import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    2. import com.example.demo.model.FileStorage;
    3. import org.apache.ibatis.annotations.Mapper;
    4. /**
    5. * 文件存储表(FileStorage)表数据库访问层
    6. */
    7. @Mapper
    8. public interface FileStorageMapper extends BaseMapper {
    9. }

    Service层

    操作数据库文件分片表的service接口 

    1. import com.baomidou.mybatisplus.extension.service.IService;
    2. import com.example.demo.model.CheckResultVo;
    3. import com.example.demo.model.FileChunk;
    4. import com.example.demo.model.FileChunkDto;
    5. /**
    6. * 文件块存储(FileChunk)表服务接口
    7. */
    8. public interface FileChunkService extends IService {
    9. /**
    10. * 校验文件
    11. * @param dto 入参
    12. * @return obj
    13. */
    14. CheckResultVo check(FileChunkDto dto);
    15. }

     操作数据库文件表的service接口 

    1. import com.baomidou.mybatisplus.extension.service.IService;
    2. import com.example.demo.model.FileChunkDto;
    3. import com.example.demo.model.FileStorage;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletResponse;
    6. /**
    7. * 文件存储表(FileStorage)表服务接口
    8. */
    9. public interface FileStorageService extends IService {
    10. /**
    11. * 文件上传接口
    12. * @param dto 入参
    13. * @return
    14. */
    15. Boolean uploadFile(FileChunkDto dto);
    16. /**
    17. * 下载文件
    18. * @param identifier
    19. * @param request
    20. * @param response
    21. */
    22. void downloadByIdentifier(String identifier, HttpServletRequest request, HttpServletResponse response);
    23. }

    Impl层

    操作数据库文件分片表的impl

    1. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    3. import com.example.demo.mapper.FileChunkMapper;
    4. import com.example.demo.model.CheckResultVo;
    5. import com.example.demo.model.FileChunk;
    6. import com.example.demo.model.FileChunkDto;
    7. import com.example.demo.service.FileChunkService;
    8. import org.springframework.stereotype.Service;
    9. import java.util.ArrayList;
    10. import java.util.List;
    11. /**
    12. * 文件块存储(FileChunk)表服务实现类
    13. */
    14. @Service
    15. public class FileChunkServiceImpl extends ServiceImpl implements FileChunkService {
    16. @Override
    17. public CheckResultVo check(FileChunkDto dto) {
    18. CheckResultVo vo = new CheckResultVo();
    19. // 1. 根据 identifier 查找数据是否存在
    20. List list = this.list(new LambdaQueryWrapper()
    21. .eq(FileChunk::getIdentifier, dto.getIdentifier())
    22. .orderByAsc(FileChunk::getChunkNumber)
    23. );
    24. // 如果是 0 说明文件不存在,则直接返回没有上传
    25. if (list.size() == 0) {
    26. vo.setUploaded(false);
    27. return vo;
    28. }
    29. // 如果不是0,则拿到第一个数据,查看文件是否分片
    30. // 如果没有分片,那么直接返回已经上穿成功
    31. FileChunk fileChunk = list.get(0);
    32. if (fileChunk.getTotalChunk() == 1) {
    33. vo.setUploaded(true);
    34. return vo;
    35. }
    36. // 处理分片
    37. ArrayList uploadedFiles = new ArrayList<>();
    38. for (FileChunk chunk : list) {
    39. uploadedFiles.add(chunk.getChunkNumber());
    40. }
    41. vo.setUploadedChunks(uploadedFiles);
    42. return vo;
    43. }
    44. }

    操作数据库文件表的impl

    1. import cn.hutool.core.bean.BeanUtil;
    2. import cn.hutool.core.io.FileUtil;
    3. import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    4. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    5. import com.example.demo.mapper.FileStorageMapper;
    6. import com.example.demo.model.FileChunk;
    7. import com.example.demo.model.FileChunkDto;
    8. import com.example.demo.model.FileStorage;
    9. import com.example.demo.service.FileChunkService;
    10. import com.example.demo.service.FileStorageService;
    11. import com.example.demo.util.BulkFileUtil;
    12. import com.example.demo.util.RedisCache;
    13. import lombok.SneakyThrows;
    14. import lombok.extern.slf4j.Slf4j;
    15. import org.springframework.beans.factory.annotation.Value;
    16. import org.springframework.stereotype.Service;
    17. import org.springframework.web.multipart.MultipartFile;
    18. import javax.annotation.Resource;
    19. import javax.servlet.http.HttpServletRequest;
    20. import javax.servlet.http.HttpServletResponse;
    21. import java.io.File;
    22. import java.io.IOException;
    23. import java.io.RandomAccessFile;
    24. import java.util.Arrays;
    25. import java.util.List;
    26. import java.util.stream.IntStream;
    27. /**
    28. * 文件存储表(FileStorage)表服务实现类
    29. */
    30. @Service
    31. @Slf4j
    32. public class FileStorageServiceImpl extends ServiceImpl implements FileStorageService {
    33. @Resource
    34. private RedisCache redisCache;
    35. /**
    36. * 默认分块大小
    37. */
    38. @Value("${file.chunk-size}")
    39. public Long defaultChunkSize;
    40. /**
    41. * 上传地址
    42. */
    43. @Value("${file.path}")
    44. private String baseFileSavePath;
    45. @Resource
    46. FileChunkService fileChunkService;
    47. @Override
    48. public Boolean uploadFile(FileChunkDto dto) {
    49. // 简单校验,如果是正式项目,要考虑其他数据的校验
    50. if (dto.getFile() == null) {
    51. throw new RuntimeException("文件不能为空");
    52. }
    53. String fullFileName = baseFileSavePath + File.separator + dto.getFilename();
    54. Boolean uploadFlag;
    55. // 如果是单文件上传
    56. if (dto.getTotalChunks() == 1) {
    57. uploadFlag = this.uploadSingleFile(fullFileName, dto);
    58. } else {
    59. // 分片上传
    60. uploadFlag = this.uploadSharding(fullFileName, dto);
    61. }
    62. // 如果本次上传成功则存储数据到 表中
    63. if (uploadFlag) {
    64. this.saveFile(dto, fullFileName);
    65. }
    66. return uploadFlag;
    67. }
    68. @SneakyThrows
    69. @Override
    70. public void downloadByIdentifier(String identifier, HttpServletRequest request, HttpServletResponse response) {
    71. FileStorage file = this.getOne(new LambdaQueryWrapper()
    72. .eq(FileStorage::getIdentifier, identifier));
    73. if (BeanUtil.isNotEmpty(file)) {
    74. File toFile = new File(baseFileSavePath + File.separator + file.getFilePath());
    75. BulkFileUtil.downloadFile(request, response, toFile);
    76. } else {
    77. throw new RuntimeException("文件不存在");
    78. }
    79. }
    80. /**
    81. * 分片上传方法
    82. * 这里使用 RandomAccessFile 方法,也可以使用 MappedByteBuffer 方法上传
    83. * 可以省去文件合并的过程
    84. *
    85. * @param fullFileName 文件名
    86. * @param dto 文件dto
    87. */
    88. private Boolean uploadSharding(String fullFileName, FileChunkDto dto) {
    89. // try 自动资源管理
    90. try (RandomAccessFile randomAccessFile = new RandomAccessFile(fullFileName, "rw")) {
    91. // 分片大小必须和前端匹配,否则上传会导致文件损坏
    92. long chunkSize = dto.getChunkSize() == 0L ? defaultChunkSize : dto.getChunkSize().longValue();
    93. // 偏移量, 意思是我从拿一个位置开始往文件写入,每一片的大小 * 已经存的块数
    94. long offset = chunkSize * (dto.getChunkNumber() - 1);
    95. // 定位到该分片的偏移量
    96. randomAccessFile.seek(offset);
    97. // 写入
    98. randomAccessFile.write(dto.getFile().getBytes());
    99. } catch (IOException e) {
    100. log.error("文件上传失败:" + e);
    101. return Boolean.FALSE;
    102. }
    103. return Boolean.TRUE;
    104. }
    105. private Boolean uploadSingleFile(String fullFileName, FileChunkDto dto) {
    106. try {
    107. File localPath = new File(fullFileName);
    108. dto.getFile().transferTo(localPath);
    109. return Boolean.TRUE;
    110. } catch (IOException e) {
    111. throw new RuntimeException(e);
    112. }
    113. }
    114. private void saveFile(FileChunkDto dto, String fileName) {
    115. FileChunk chunk = BeanUtil.copyProperties(dto, FileChunk.class);
    116. chunk.setFileName(dto.getFilename());
    117. chunk.setTotalChunk(dto.getTotalChunks());
    118. fileChunkService.save(chunk);
    119. // 这里每次上传切片都存一下缓存,
    120. redisCache.setCacheListByOne(dto.getIdentifier(), dto.getChunkNumber());
    121. // 如果所有快都上传完成,那么在文件记录表中存储一份数据
    122. List chunkList = redisCache.getCacheList(dto.getIdentifier());
    123. Integer totalChunks = dto.getTotalChunks();
    124. int[] chunks = IntStream.rangeClosed(1, totalChunks).toArray();
    125. // 从缓存中查看是否所有块上传完成,
    126. if (IntStream.rangeClosed(1, totalChunks).allMatch(chunkList::contains)) {
    127. // 所有分片上传完成,组合分片并保存到数据库中
    128. String name = dto.getFilename();
    129. MultipartFile file = dto.getFile();
    130. FileStorage fileStorage = new FileStorage();
    131. fileStorage.setRealName(file.getOriginalFilename());
    132. fileStorage.setFileName(fileName);
    133. fileStorage.setSuffix(FileUtil.getSuffix(name));
    134. fileStorage.setFileType(file.getContentType());
    135. fileStorage.setSize(dto.getTotalSize());
    136. fileStorage.setIdentifier(dto.getIdentifier());
    137. fileStorage.setFilePath(dto.getRelativePath());
    138. this.save(fileStorage);
    139. }
    140. }
    141. }

    Util工具类

    文件相关的工具类 

    1. import lombok.extern.slf4j.Slf4j;
    2. import org.apache.tomcat.util.http.fileupload.IOUtils;
    3. import sun.misc.BASE64Encoder;
    4. import javax.servlet.http.HttpServletRequest;
    5. import javax.servlet.http.HttpServletResponse;
    6. import java.io.File;
    7. import java.io.FileInputStream;
    8. import java.io.IOException;
    9. import java.io.UnsupportedEncodingException;
    10. import java.net.URLEncoder;
    11. /**
    12. * 文件相关的处理
    13. */
    14. @Slf4j
    15. public class BulkFileUtil {
    16. /**
    17. * 文件下载
    18. * @param request
    19. * @param response
    20. * @param file
    21. * @throws UnsupportedEncodingException
    22. */
    23. public static void downloadFile(HttpServletRequest request, HttpServletResponse response, File file) throws UnsupportedEncodingException {
    24. response.setCharacterEncoding(request.getCharacterEncoding());
    25. response.setContentType("application/octet-stream");
    26. FileInputStream fis = null;
    27. String filename = filenameEncoding(file.getName(), request);
    28. try {
    29. fis = new FileInputStream(file);
    30. response.setHeader("Content-Disposition", String.format("attachment;filename=%s", filename));
    31. IOUtils.copy(fis, response.getOutputStream());
    32. response.flushBuffer();
    33. } catch (Exception e) {
    34. log.error(e.getMessage(), e);
    35. } finally {
    36. if (fis != null) {
    37. try {
    38. fis.close();
    39. } catch (IOException e) {
    40. log.error(e.getMessage(), e);
    41. }
    42. }
    43. }
    44. }
    45. /**
    46. * 适配不同的浏览器,确保文件名字正常
    47. *
    48. * @param filename
    49. * @param request
    50. * @return
    51. * @throws UnsupportedEncodingException
    52. */
    53. public static String filenameEncoding(String filename, HttpServletRequest request) throws UnsupportedEncodingException {
    54. // 获得请求头中的User-Agent
    55. String agent = request.getHeader("User-Agent");
    56. // 根据不同的客户端进行不同的编码
    57. if (agent.contains("MSIE")) {
    58. // IE浏览器
    59. filename = URLEncoder.encode(filename, "utf-8");
    60. } else if (agent.contains("Firefox")) {
    61. // 火狐浏览器
    62. BASE64Encoder base64Encoder = new BASE64Encoder();
    63. filename = "=?utf-8?B?" + base64Encoder.encode(filename.getBytes("utf-8")) + "?=";
    64. } else {
    65. // 其它浏览器
    66. filename = URLEncoder.encode(filename, "utf-8");
    67. }
    68. return filename;
    69. }
    70. }

    redis的工具类 

    1. import org.springframework.beans.factory.annotation.Autowired;
    2. import org.springframework.data.redis.core.BoundSetOperations;
    3. import org.springframework.data.redis.core.HashOperations;
    4. import org.springframework.data.redis.core.RedisTemplate;
    5. import org.springframework.data.redis.core.ValueOperations;
    6. import org.springframework.stereotype.Component;
    7. import java.util.*;
    8. import java.util.concurrent.TimeUnit;
    9. /**
    10. * spring redis 工具类
    11. *
    12. **/
    13. @Component
    14. public class RedisCache
    15. {
    16. @Autowired
    17. public RedisTemplate redisTemplate;
    18. /**
    19. * 缓存数据
    20. *
    21. * @param key 缓存的键值
    22. * @param data 待缓存的数据
    23. * @return 缓存的对象
    24. */
    25. public long setCacheListByOne(final String key, final T data)
    26. {
    27. Long count = redisTemplate.opsForList().rightPush(key, data);
    28. if (count != null && count == 1) {
    29. redisTemplate.expire(key, 60, TimeUnit.SECONDS);
    30. }
    31. return count == null ? 0 : count;
    32. }
    33. /**
    34. * 获得缓存的list对象
    35. *
    36. * @param key 缓存的键值
    37. * @return 缓存键值对应的数据
    38. */
    39. public List getCacheList(final String key)
    40. {
    41. return redisTemplate.opsForList().range(key, 0, -1);
    42. }
    43. }

    响应体工具类 

    1. import lombok.Data;
    2. import java.io.Serializable;
    3. @Data
    4. public class ResponseResult implements Serializable {
    5. private Boolean success;
    6. private Integer code;
    7. private String msg;
    8. private T data;
    9. public ResponseResult() {
    10. this.success=true;
    11. this.code = HttpCodeEnum.SUCCESS.getCode();
    12. this.msg = HttpCodeEnum.SUCCESS.getMsg();
    13. }
    14. public ResponseResult(Integer code, T data) {
    15. this.code = code;
    16. this.data = data;
    17. }
    18. public ResponseResult(Integer code, String msg, T data) {
    19. this.code = code;
    20. this.msg = msg;
    21. this.data = data;
    22. }
    23. public ResponseResult(Integer code, String msg) {
    24. this.code = code;
    25. this.msg = msg;
    26. }
    27. public ResponseResult error(Integer code, String msg) {
    28. this.success=false;
    29. this.code = code;
    30. this.msg = msg;
    31. return this;
    32. }
    33. public static ResponseResult ok(Object data) {
    34. ResponseResult result = new ResponseResult();
    35. result.setData(data);
    36. return result;
    37. }
    38. }

    controller层

    返回对应的页面 

    1. import org.springframework.stereotype.Controller;
    2. import org.springframework.web.bind.annotation.GetMapping;
    3. import org.springframework.web.bind.annotation.PathVariable;
    4. import org.springframework.web.bind.annotation.RequestMapping;
    5. /**
    6. * 返回对于页面
    7. */
    8. @Controller
    9. @RequestMapping("/page")
    10. public class PageController {
    11. @GetMapping("/{path}")
    12. public String toPage(@PathVariable String path) {
    13. return path;
    14. }
    15. }

    文件上传接口 

    1. import com.example.demo.model.CheckResultVo;
    2. import com.example.demo.model.FileChunkDto;
    3. import com.example.demo.service.FileChunkService;
    4. import com.example.demo.service.FileStorageService;
    5. import com.example.demo.util.ResponseResult;
    6. import org.springframework.web.bind.annotation.*;
    7. import javax.annotation.Resource;
    8. import javax.servlet.http.HttpServletRequest;
    9. import javax.servlet.http.HttpServletResponse;
    10. import java.io.IOException;
    11. /**
    12. * 文件存储表(FileStorage)表控制层
    13. */
    14. @RestController
    15. @RequestMapping("fileStorage")
    16. public class FileStorageController {
    17. @Resource
    18. private FileStorageService fileStorageService;
    19. @Resource
    20. private FileChunkService fileChunkService;
    21. /**
    22. * 本接口为校验接口,即上传前,先根据本接口查询一下 服务器是否存在该文件
    23. * @param dto 入参
    24. * @return vo
    25. */
    26. @GetMapping("/upload")
    27. public ResponseResult checkUpload(FileChunkDto dto) {
    28. return ResponseResult.ok(fileChunkService.check(dto));
    29. }
    30. /**
    31. * 本接口为实际上传接口
    32. * @param dto 入参
    33. * @param response response 配合前端返回响应的状态码
    34. * @return boolean
    35. */
    36. @PostMapping("/upload")
    37. public ResponseResult upload(FileChunkDto dto, HttpServletResponse response) {
    38. try {
    39. Boolean status = fileStorageService.uploadFile(dto);
    40. if (status) {
    41. return ResponseResult.ok("上传成功");
    42. } else {
    43. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    44. return ResponseResult.error("上传失败");
    45. }
    46. } catch (Exception e) {
    47. // 这个code 是根据前端组件的特性来的,也可以自己定义规则
    48. response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    49. return ResponseResult.error("上传失败");
    50. }
    51. }
    52. /**
    53. * 下载接口,这里只做了普通的下载
    54. * @param request req
    55. * @param response res
    56. * @param identifier md5
    57. * @throws IOException 异常
    58. */
    59. @GetMapping(value = "/download/{identifier}")
    60. public void downloadByIdentifier(HttpServletRequest request, HttpServletResponse response, @PathVariable("identifier") String identifier) throws IOException {
    61. fileStorageService.downloadByIdentifier(identifier, request, response);
    62. }
    63. }

    前端部分

    下载插件

    vue.js
    element-ui.js
    axios.js
    vue-uploader.js
    spark-md5.min.js
    zh-CN.js

    页面部分

    页面主要结构 

    插件引入 

    1. html>
    2. <html xmlns:th="http://www.thymeleaf.org">
    3. <head th:fragment="base">
    4. <meta charset="utf-8">
    5. <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    6. <script th:src="@{/plugins/vue.js}" type="text/javascript">script>
    7. <script th:src="@{/plugins/element/element-ui.js}" type="text/javascript">script>
    8. <link th:href="@{/plugins/element/css/element-ui.css}" rel="stylesheet" type="text/css">
    9. <script th:src="@{/plugins/axios.js}" type="text/javascript">script>
    10. <script th:src="@{/plugins/vue-uploader.js}" type="text/javascript">script>
    11. <script th:src="@{/plugins/spark-md5.min.js}" type="text/javascript">script>
    12. <script th:src="@{/plugins/element/zh-CN.js}">script>
    13. <script type="text/javascript">
    14. ELEMENT.locale(ELEMENT.lang.zhCN)
    15. // 添加响应拦截器
    16. axios.interceptors.response.use(response => {
    17. if (response.request.responseType === 'blob') {
    18. return response
    19. }
    20. // 对响应数据做点什么
    21. const res = response.data
    22. if (!!res && res.code === 1) { // 公共处理失败请求
    23. ELEMENT.Message({message: "异常:" + res.msg, type: "error"})
    24. return Promise.reject(new Error(res.msg || 'Error'));
    25. } else {
    26. return res
    27. }
    28. }, function (error) {
    29. // 对响应错误做点什么
    30. return Promise.reject(error);
    31. });
    32. script>
    33. head>
    34. html>

    上传文件主页 

    1. html>
    2. <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns="http://www.w3.org/1999/html">
    3. <head>
    4. <meta charset="UTF-8">
    5. <title>大文件上传title>
    6. <head th:include="/base::base"><title>title>head>
    7. <link href="upload.css" rel="stylesheet" type="text/css">
    8. head>
    9. <body>
    10. <div id="app">
    11. <uploader
    12. ref="uploader"
    13. :options="options"
    14. :autoStart="false"
    15. :file-status-text="fileStatusText"
    16. @file-added="onFileAdded"
    17. @file-success="onFileSuccess"
    18. @file-error="onFileError"
    19. @file-progress="onFileProgress"
    20. class="uploader-example"
    21. >
    22. <uploader-unsupport>uploader-unsupport>
    23. <uploader-drop>
    24. <p>拖动文件到这里上传p>
    25. <uploader-btn>选择文件uploader-btn>
    26. uploader-drop>
    27. <uploader-list>
    28. <el-collapse v-model="activeName" accordion>
    29. <el-collapse-item title="文件列表" name="1">
    30. <ul class="file-list">
    31. <li v-for="file in uploadFileList" :key="file.id">
    32. <uploader-file :file="file" :list="true" ref="uploaderFile">
    33. <div slot-scope="props" style="display: flex;align-items: center;height: 100%;">
    34. <el-progress
    35. style="width: 85%"
    36. :stroke-width="18"
    37. :show-text="true"
    38. :text-inside="true"
    39. :format="e=> showDetail(e,props)"
    40. :percentage="percentage(props)"
    41. :color="e=>progressColor(e,props)">
    42. el-progress>
    43. <el-button :icon="icon" circle v-if="props.paused || props.isUploading"
    44. @click="pause(file)" size="mini">el-button>
    45. <el-button icon="el-icon-close" circle @click="remove(file)"
    46. size="mini">el-button>
    47. <el-button icon="el-icon-download" circle v-if="props.isComplete"
    48. @click="download(file)"
    49. size="mini">el-button>
    50. div>
    51. uploader-file>
    52. li>
    53. <div class="no-file" v-if="!uploadFileList.length">
    54. <i class="icon icon-empty-file">i> 暂无待上传文件
    55. div>
    56. ul>
    57. el-collapse-item>
    58. el-collapse>
    59. uploader-list>
    60. uploader>
    61. div>
    62. <script>
    63. // 分片大小,20MB
    64. const CHUNK_SIZE = 20 * 1024 * 1024;
    65. new Vue({
    66. el: '#app',
    67. data() {
    68. return {
    69. options: {
    70. // 上传地址
    71. target: "/fileStorage/upload",
    72. // 是否开启服务器分片校验。默认为 true
    73. testChunks: true,
    74. // 真正上传的时候使用的 HTTP 方法,默认 POST
    75. uploadMethod: "post",
    76. // 分片大小
    77. chunkSize: CHUNK_SIZE,
    78. // 并发上传数,默认为 3
    79. simultaneousUploads: 3,
    80. /**
    81. * 判断分片是否上传,秒传和断点续传基于此方法
    82. * 这里根据实际业务来 用来判断哪些片已经上传过了 不用再重复上传了 [这里可以用来写断点续传!!!]
    83. * 检查某个文件块是否已经上传到服务器,并根据检查结果返回布尔值。
    84. * chunk 表示待检查的文件块,是一个对象类型包含 offset 和 blob 两个字段。offset 表示该块在文件中的偏移量blob 表示该块对应的二进制数据
    85. * message 则表示服务器返回的响应消息,是一个字符串类型。
    86. */
    87. checkChunkUploadedByResponse: (chunk, message) => {
    88. console.log("message", message)
    89. // message是后台返回
    90. let messageObj = JSON.parse(message);
    91. let dataObj = messageObj.data;
    92. if (dataObj.uploaded !== null) {
    93. return dataObj.uploaded;
    94. }
    95. // 判断文件或分片是否已上传,已上传返回 true
    96. // 这里的 uploadedChunks 是后台返回
    97. // 判断 data.uploadedChunks 数组(或空数组)中是否包含 chunk.offset + 1 的值。
    98. // chunk.offset + 1 表示当前文件块在整个文件中的排序位置(从 1 开始计数),
    99. // 如果该值存在于 data.uploadedChunks 中,则说明当前文件块已经上传过了,返回 true,否则返回 false。
    100. return (dataObj.uploadedChunks || []).indexOf(chunk.offset + 1) >= 0;
    101. },
    102. parseTimeRemaining: function (timeRemaining, parsedTimeRemaining) {
    103. //格式化时间
    104. return parsedTimeRemaining
    105. .replace(/\syears?/, "年")
    106. .replace(/\days?/, "天")
    107. .replace(/\shours?/, "小时")
    108. .replace(/\sminutes?/, "分钟")
    109. .replace(/\sseconds?/, "秒");
    110. },
    111. },
    112. // 修改上传状态
    113. fileStatusTextObj: {
    114. success: "上传成功",
    115. error: "上传错误",
    116. uploading: "正在上传",
    117. paused: "停止上传",
    118. waiting: "等待中",
    119. },
    120. uploadFileList: [],
    121. collapse: true,
    122. activeName: 1,
    123. icon: `el-icon-video-pause`
    124. }
    125. },
    126. methods: {
    127. onFileAdded(file, event) {
    128. console.log("eeeee",event)
    129. // event.preventDefault();
    130. this.uploadFileList.push(file);
    131. console.log("file :>> ", file);
    132. // 有时 fileType为空,需截取字符
    133. console.log("文件类型:" + file.fileType + "文件大小:" + file.size + "B");
    134. // 1. todo 判断文件类型是否允许上传
    135. // 2. 计算文件 MD5 并请求后台判断是否已上传,是则取消上传
    136. console.log("校验MD5");
    137. this.getFileMD5(file, (md5) => {
    138. if (md5 !== "") {
    139. // 修改文件唯一标识
    140. file.uniqueIdentifier = md5;
    141. // 请求后台判断是否上传
    142. // 恢复上传
    143. file.resume();
    144. }
    145. });
    146. },
    147. onFileSuccess(rootFile, file, response, chunk) {
    148. console.log("上传成功", rootFile, file, response, chunk);
    149. // 这里可以做一些上传成功之后的事情,比如,如果后端需要合并的话,可以通知到后端合并
    150. },
    151. onFileError(rootFile, file, message, chunk) {
    152. console.log("上传出错:" + message, rootFile, file, message, chunk);
    153. },
    154. onFileProgress(rootFile, file, chunk) {
    155. console.log(`当前进度:${Math.ceil(file._prevProgress * 100)}%`);
    156. },
    157. // 计算文件的MD5值
    158. getFileMD5(file, callback) {
    159. let spark = new SparkMD5.ArrayBuffer();
    160. let fileReader = new FileReader();
    161. //获取文件分片对象(注意它的兼容性,在不同浏览器的写法不同)
    162. let blobSlice =
    163. File.prototype.slice ||
    164. File.prototype.mozSlice ||
    165. File.prototype.webkitSlice;
    166. // 当前分片下标
    167. let currentChunk = 0;
    168. // 分片总数(向下取整)
    169. let chunks = Math.ceil(file.size / CHUNK_SIZE);
    170. // MD5加密开始时间
    171. let startTime = new Date().getTime();
    172. // 暂停上传
    173. file.pause();
    174. loadNext();
    175. // fileReader.readAsArrayBuffer操作会触发onload事件
    176. fileReader.onload = function (e) {
    177. // console.log("currentChunk :>> ", currentChunk);
    178. // 通过 e.target.result 获取到当前分片的内容,并将其追加到 MD5 计算实例 spark 中,以便后续计算整个文件的 MD5 值。
    179. spark.append(e.target.result);
    180. // 通过比较当前分片的索引 currentChunk 是否小于总分片数 chunks 判断是否还有下一个分片需要读取。
    181. if (currentChunk < chunks) {
    182. // 如果存在下一个分片,则将当前分片索引递增并调用 loadNext() 函数加载下一个分片;
    183. currentChunk++;
    184. loadNext();
    185. } else {
    186. // 否则,表示所有分片已经读取完毕,可以进行最后的 MD5 计算。
    187. // 该文件的md5值
    188. let md5 = spark.end();
    189. console.log(
    190. `MD5计算完毕:${md5},耗时:${new Date().getTime() - startTime} ms.`
    191. );
    192. // 回调传值md5
    193. callback(md5);
    194. }
    195. };
    196. fileReader.onerror = function () {
    197. this.$message.error("文件读取错误");
    198. file.cancel();
    199. };
    200. // 加载下一个分片
    201. function loadNext() {
    202. // start 的计算方式为当前分片的索引乘以分片大小 CHUNK_SIZE
    203. const start = currentChunk * CHUNK_SIZE;
    204. // end 的计算方式为 start 加上 CHUNK_SIZE,但如果超过了文件的总大小,则取文件的大小作为结束位置。
    205. const end = start + CHUNK_SIZE >= file.size ? file.size : start + CHUNK_SIZE;
    206. // 文件分片操作,读取下一分片(fileReader.readAsArrayBuffer操作会触发onload事件)
    207. // 通过调用 blobSlice.call(file.file, start, end) 方法获取当前分片的 Blob 对象,即指定开始和结束位置的文件分片。
    208. // 接着,使用 fileReader.readAsArrayBuffer() 方法读取该 Blob 对象的内容,从而触发 onload 事件,继续进行文件的处理
    209. fileReader.readAsArrayBuffer(blobSlice.call(file.file, start, end));
    210. }
    211. },
    212. // 上传状态文本
    213. fileStatusText(status, response) {
    214. if (status === "md5") {
    215. return "校验MD5";
    216. } else {
    217. return this.fileStatusTextObj[status];
    218. }
    219. },
    220. // 点击暂停
    221. pause(file, id) {
    222. console.log("file :>> ", file);
    223. console.log("id :>> ", id);
    224. if (file.paused) {
    225. file.resume();
    226. this.icon = 'el-icon-video-pause'
    227. } else {
    228. this.icon = 'el-icon-video-play'
    229. file.pause();
    230. }
    231. },
    232. // 点击删除
    233. remove(file) {
    234. this.uploadFileList.findIndex((item, index) => {
    235. if (item.id === file.id) {
    236. this.$nextTick(() => {
    237. this.uploadFileList.splice(index, 1);
    238. });
    239. }
    240. });
    241. },
    242. showDetail(percentage, props) {
    243. let fileName = props.file.name;
    244. let isComplete = props.isComplete
    245. let formatUpload = this.formatFileSize(props.uploadedSize, 2);
    246. let fileSize = `${props.formatedSize}`;
    247. let timeRemaining = !isComplete ? ` 剩余时间:${props.formatedTimeRemaining}` : ''
    248. let uploaded = !isComplete ? ` 已上传:${formatUpload} / ${fileSize}` : ` 大小:${fileSize}`
    249. let speed = !isComplete ? ` 速度:${props.formatedAverageSpeed}` : ''
    250. if (props.error) {
    251. return `${fileName} \t 上传失败`
    252. }
    253. return `${fileName} \t ${speed} \t ${uploaded} \t ${timeRemaining} \t 进度:${percentage} %`;
    254. },
    255. // 显示进度
    256. percentage(props) {
    257. let progress = props.progress.toFixed(2) * 100;
    258. return progress - 1 < 0 ? 0 : progress;
    259. },
    260. // 控制下进度条的颜色 ,异常的情况下显示红色
    261. progressColor(e, props) {
    262. if (props.error) {
    263. return `#f56c6c`
    264. }
    265. if (e > 0) {
    266. return `#1989fa`
    267. }
    268. },
    269. // 点击下载
    270. download(file, id) {
    271. console.log("file:>> ", file);
    272. window.location.href = `/fileStorage/download/${file.uniqueIdentifier}`;
    273. },
    274. formatFileSize(bytes, decimalPoint = 2) {
    275. if (bytes == 0) return "0 Bytes";
    276. let k = 1000,
    277. sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"],
    278. i = Math.floor(Math.log(bytes) / Math.log(k));
    279. return (
    280. parseFloat((bytes / Math.pow(k, i)).toFixed(decimalPoint)) + " " + sizes[i]
    281. );
    282. }
    283. },
    284. })
    285. script>
    286. body>
    287. html>

    上传页面对应的样式 

    1. .uploader-example {
    2. width: 880px;
    3. padding: 15px;
    4. margin: 40px auto 0;
    5. font-size: 12px;
    6. box-shadow: 0 0 10px rgba(0, 0, 0, 0.4);
    7. }
    8. .uploader-example .uploader-btn {
    9. margin-right: 4px;
    10. }
    11. .uploader-example .uploader-list {
    12. max-height: 440px;
    13. overflow: auto;
    14. overflow-x: hidden;
    15. overflow-y: auto;
    16. }
    17. #global-uploader {
    18. position: fixed;
    19. z-index: 20;
    20. right: 15px;
    21. bottom: 15px;
    22. width: 550px;
    23. }
    24. .uploader-file {
    25. height: 90px;
    26. }
    27. .uploader-file-meta {
    28. display: none !important;
    29. }
    30. .operate {
    31. flex: 1;
    32. text-align: right;
    33. }
    34. .file-list {
    35. position: relative;
    36. height: 300px;
    37. overflow-x: hidden;
    38. overflow-y: auto;
    39. background-color: #fff;
    40. padding: 0px;
    41. margin: 0 auto;
    42. transition: all 0.5s;
    43. }
    44. .uploader-file-size {
    45. width: 15% !important;
    46. }
    47. .uploader-file-status {
    48. width: 32.5% !important;
    49. text-align: center !important;
    50. }
    51. li {
    52. background-color: #fff;
    53. list-style-type: none;
    54. }
    55. .no-file {
    56. position: absolute;
    57. top: 50%;
    58. left: 50%;
    59. transform: translate(-50%, -50%);
    60. font-size: 16px;
    61. }
    62. .uploader-file-name {
    63. width: 36% !important;
    64. }
    65. .uploader-file-actions {
    66. float: right !important;
    67. }
    68. /deep/ .el-progress-bar {
    69. width: 95%;
    70. }

    测试

    访问localhost:7125/page/index.html

     选择上传文件

    上传成功

     上传文件目录

  • 相关阅读:
    Linux部署Spark 本地模式
    金仓数据库KingbaseES服务器应用参考手册--9. sys_test_fsync
    如何深入学习Java并发编程?
    IIS6.0 PUT上传漏洞
    JUC - 线程基础
    Python 变量类型
    计算机操作系统 第四章 存储器管理(1)
    C/C++数据结构——队列
    .\missyou-0.0.1-SNAPSHOT.jar中没有主清单属性
    企业搭建网站用哪种服务器
  • 原文地址:https://blog.csdn.net/qq_63431773/article/details/133563174