当前位置: 首页 > ai >正文

结合redis实现文件分片秒传断点续传

# 工作流程

1.  **秒传**

-   -   前端计算文件MD5并请求`  /check `
    -   Redis中存在MD5记录 → 直接返回成功

2.  **分片上传**

-   -   检查分片是否已上传(通过Redis Set)
    -   只上传缺失分片
    -   每个分片独立保存到临时目录

3.  **断点续传**

-   -   刷新页面后重新计算MD5
    -   检查已上传分片列表
    -   继续上传剩余分片

4.  **合并文件**

-   -   所有分片上传完成后触发合并
    -   按索引顺序合并分片
    -   清理临时文件和Redis记录

# 实现的思路(容易懂一点的)

1.前端计算上传文件的MD5值,若redis中有对应的MD5值,则直接说明秒传成功

2.后端通过redis记录文件分片详情,我这里使用的是set,可以根据需要使用不同的类型

3.前端开始上传后,每上传成功一段分片都要记录在redis,若此时暂停上传且此时分片未上传完成则视为分片未成功上传,不需要在redis记录

4.续传后,前端重新计算MD5值,重新开始上传redis中记录缺失的分片

5.上传成功后,对于分片的记录数进行删除,以文件MD5值为value重新记录说明此文件已成功上传(此处是为了实现秒传)

6.这里我多加了一个oss,当我上传文件成功后我调用oss工具类将其上传到oss得到返回的url给前端

下面是后端的接口实现代码(代码仅供参考,可以直接使用但功能并不完善,仅作demo):
注意:文件路径记得改成自己的(建议单独建一个文件夹或者使用临时文件)

# 秒传验证&检查分片

```
@PostMapping("/check")
public ResponseEntity<?> checkFile(@RequestParam("fileMd5") String fileMd5,
                                   @RequestParam("chunkIndex") Integer chunkIndex) {
    // 1. 检查文件是否已存在 (秒传)
    if (Boolean.TRUE.equals(redisTemplate.hasKey("FILE_MD5:" + fileMd5))) {
        return ResponseEntity.ok(Map.of("exist", true, "uploaded", true));
    }

    // 2. 检查当前分片是否已上传
    String chunkKey = "CHUNKS:" + fileMd5;
    Boolean isChunkUploaded = redisTemplate.opsForSet().isMember(chunkKey, chunkIndex);

    return ResponseEntity.ok(Map.of(
            "exist", false,
            "chunkUploaded", Boolean.TRUE.equals(isChunkUploaded)
    ));
}
```

# 分片上传

```
@PostMapping("/upload")
public ResponseEntity<?> uploadChunk(@RequestParam("file") MultipartFile file,
                                     @RequestParam("fileMd5") String fileMd5,
                                     @RequestParam("chunkIndex") Integer chunkIndex) throws IOException {

    // 1. 创建临时目录
    File chunkDir = new File(tempDir + fileMd5);
    if (!chunkDir.exists()) {
        chunkDir.mkdirs();
    }

    // 2. 保存分片文件
    File chunkFile = new File(chunkDir, chunkIndex.toString());
    file.transferTo(chunkFile);

    // 3. 在Redis中记录已上传分片
    String chunkKey = "CHUNKS:" + fileMd5;
    redisTemplate.opsForSet().add(chunkKey, chunkIndex);

    return ResponseEntity.ok().build();
}
```

# 合并分片

```
@PostMapping("/merge")
public ResponseEntity<?> mergeChunks(@RequestParam("fileMd5") String fileMd5,
                                     @RequestParam("fileName") String fileName,
                                     @RequestParam("totalChunks") Integer totalChunks) throws IOException {

    // 1. 验证是否所有分片已上传
    String chunkKey = "CHUNKS:" + fileMd5;
    Long uploadedCount = redisTemplate.opsForSet().size(chunkKey);

    if (uploadedCount == null || uploadedCount != (long) totalChunks) {
        return ResponseEntity.status(HttpStatus.PARTIAL_CONTENT).build();
    }

    // 2. 合并文件
    File destFile = new File("E:/code/file/final/" + fileName); // 推荐使用正斜杠
    if (!destFile.getParentFile().exists()) {
        destFile.getParentFile().mkdirs();
    }

    try (FileOutputStream fos = new FileOutputStream(destFile)) {
        for (int i = 0; i < totalChunks; i++) {
            File chunkFile = new File(tempDir + fileMd5, String.valueOf(i));
            FileUtils.copyFile(chunkFile, fos);
            chunkFile.delete(); // 删除分片
        }
    }

    // ✅ 新增:将合并后的文件转为 MultipartFile 并上传 OSS
    MultipartFile multipartFile = convertToMultipartFile(destFile, fileName);
    String ossUrl = ossUtil.uploadMultipartFile(multipartFile);

    // 3. 清理临时目录
    FileUtils.deleteDirectory(new File(tempDir + fileMd5));

    // 4. 记录文件MD5(秒传标识)
    redisTemplate.opsForValue().set("FILE_MD5:" + fileMd5, ossUrl); // 存储的是 OSS 地址

    // 5. 清理Redis分片记录
    redisTemplate.delete(chunkKey);

    // 6. 删除本地合并后的文件(可选)
     destFile.delete();

    log.info("文件url:"+ossUrl);
    // 7. 返回 OSS URL 给前端
    return ResponseEntity.ok(Map.of(
            "url", ossUrl
    ));
}

    //文件转换类
    private MultipartFile convertToMultipartFile(File file, String originalFilename) throws IOException {
        String contentType = Files.probeContentType(file.toPath());
        byte[] content = Files.readAllBytes(file.toPath());

        return new CustomMultipartFile(content, originalFilename, contentType);
    }
```

# 文件转换工具类

```
public class CustomMultipartFile implements MultipartFile {

    private final byte[] content;
    private final String originalFilename;
    private final String contentType;

    public CustomMultipartFile(byte[] content, String originalFilename, String contentType) {
        this.content = content;
        this.originalFilename = originalFilename;
        this.contentType = contentType;
    }

    @Override
    public String getName() {
        return "file";
    }

    @Override
    public String getOriginalFilename() {
        return originalFilename;
    }

    @Override
    public String getContentType() {
        return contentType;
    }

    @Override
    public boolean isEmpty() {
        return content == null || content.length == 0;
    }

    @Override
    public long getSize() {
        return content.length;
    }

    @Override
    public byte[] getBytes() throws IOException {
        return content;
    }

    @Override
    public InputStream getInputStream() throws IOException {
        return new ByteArrayInputStream(content);
    }

    @Override
    public void transferTo(File dest) throws IOException, IllegalStateException {
        Files.write(dest.toPath(), content);
    }
}
```

# 下面是oss配置类和工具类:

```
/**
 * OSS 文件管理服务
 */
@Log4j2
@Component
public class OssUtil {
 
    /** 自动注入 OssConfig 类型的 Bean */
    @Autowired
    private OssConfig ossConfig;
 
    /** 定义访问前缀,用于构建文件的完整访问路径 */
    @Value("${aliyun.oss.accessPre}")
    private String accessPre;
 
    /** 定义存储桶名称,方便在上传和下载时引用 */
    @Value("${aliyun.oss.bucketName}")
    private String bucketName;
 
    /**
     * 默认路径上传本地文件
     *
     * @param filePath 本地文件路径
     * @return 上传后的文件访问路径
     */
    public String uploadFile(String filePath) {
        return uploadFileForBucket(bucketName, getOssFilePath(filePath), filePath);
    }
 
    /**
     * 默认路径上传 MultipartFile 文件
     *
     * @param multipartFile 待上传的文件
     * @return 上传后的文件访问路径
     */
    public String uploadMultipartFile(MultipartFile multipartFile) {
        return uploadMultipartFile(bucketName, getOssFilePath(multipartFile.getOriginalFilename()), multipartFile);
    }
 
    /**
     * 上传 MultipartFile 类型文件到指定 Bucket
     *
     * @param bucketName    实例名称
     * @param ossPath       OSS 存储路径
     * @param multipartFile 待上传的文件
     * @return 上传后的文件访问路径
     */
    public String uploadMultipartFile(String bucketName, String ossPath, MultipartFile multipartFile) {
        try (InputStream inputStream = multipartFile.getInputStream()) {
            uploadFileInputStreamForBucket(bucketName, ossPath, inputStream);
        } catch (IOException e) {
            log.error("上传文件失败: {}", e.getMessage(), e);
            return null;
        }
        return accessPre + ossPath;
    }
 
    /**
     * 使用 File 上传文件
     *
     * @param bucketName 实例名称
     * @param ossPath    OSS 存储路径
     * @param filePath   本地文件路径
     * @return 上传后的文件访问路径
     */
    public String uploadFileForBucket(String bucketName, String ossPath, String filePath) {
        PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, ossPath, new File(filePath));
        ossConfig.init().putObject(putObjectRequest);
        return accessPre + ossPath;
    }
 
    /**
     * 使用文件流上传到指定的 Bucket 实例
     *
     * @param bucketName  实例名称
     * @param ossPath     OSS 存储路径
     * @param inputStream 文件输入流
     */
    public void uploadFileInputStreamForBucket(String bucketName, String ossPath, InputStream inputStream) {
        ossConfig.init().putObject(bucketName, ossPath, inputStream);
    }
 
    /**
     * 下载文件
     *
     * @param ossFilePath OSS 存储路径
     * @param filePath    本地文件路径
     */
    public void downloadFile(String ossFilePath, String filePath) {
        downloadFileForBucket(bucketName, ossFilePath, filePath);
    }
 
    /**
     * 从指定 Bucket 下载文件
     *
     * @param bucketName  实例名称
     * @param ossFilePath OSS 存储路径
     * @param filePath    本地文件路径
     */
    public void downloadFileForBucket(String bucketName, String ossFilePath, String filePath) {
        ossConfig.init().getObject(new GetObjectRequest(bucketName, ossFilePath), new File(filePath));
    }
 
    /**
     * 获取默认 OSS 存储路径
     *
     * @return 默认 OSS 存储路径
     */
    public String getOssDefaultPath() {
        LocalDateTime now = LocalDateTime.now();
        return String.format("%d/%d/%d/%d/%d/",
                now.getYear(),
                now.getMonthValue(),
                now.getDayOfMonth(),
                now.getHour(),
                now.getMinute());
    }
 
    /**
     * 生成 OSS 文件路径
     *
     * @param filePath 本地文件路径
     * @return OSS 文件路径
     */
    public String getOssFilePath(String filePath) {
        String fileSuffix = filePath.substring(filePath.lastIndexOf(".") + 1);
        return getOssDefaultPath() + UUID.randomUUID() + "." + fileSuffix;
    }
}
```

```
/**
 * OSS初始化配置
 */
@Log4j2
@Configuration
public class OssConfig {
    /**
     * 配置文件中读取阿里云 OSS 的 endpoint,注入到 endPoint 变量中
     */
    @Value("${aliyun.oss.endPoint}")
    private String endPoint;
 
    /**
     * 从配置文件中读取阿里云 OSS 的 accessKeyId,注入到 accessKeyId 变量中
     */
    @Value("${aliyun.oss.accessKeyId}")
    private String accessKeyId;
 
    /**
     * 从配置文件中读取阿里云 OSS 的 accessKeySecret,注入到 accessKeySecret 变量中
     */
    @Value("${aliyun.oss.accessKeySecret}")
    private String accessKeySecret;
 
    private OSS ossClient;
 
    @Bean
    public OSS init() {
        // 如果 OSS 客户端尚未初始化,则进行初始化
        if (ossClient == null) {
            // 使用 OSSClientBuilder 构建 OSS 客户端,传入 endpoint、accessKeyId 和 accessKeySecret
            ossClient = createOSSClient();
 
            // 记录日志,表示连接成功
            log.info("OSS服务连接成功!");
        }
        // 返回初始化好的 OSS 客户端实例
        return ossClient;
    }
 
    /**
     * 创建 OSS 客户端的方法
     */
    private OSS createOSSClient() {
        return new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
    }
 
    @PreDestroy
    public void destroy() {
        // 关闭 OSS 客户端
        if (ossClient != null) {
            // 调用 shutdown() 方法关闭 OSS 客户端
            ossClient.shutdown();
            // 记录日志,确认客户端已成功关闭
            log.info("OSS客户端已成功关闭。");
        }
    }
}
```

```
<!--aliyun-oss-->
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
    <version>3.17.4</version>
</dependency>
```
# 下面是前端代码(提供示例代码、仅供参考、完整代码地址看最下面):
# ```
<template>
  <div>
    <input type="file" @change="handleFileSelect" ref="fileInput">
    <el-button @click="startUpload">开始上传</el-button>
    <div>进度: {{ progress }}%</div>
  </div>
</template>

<script>
import axios from 'axios';
import SparkMD5 from 'spark-md5'; // npm install spark-md5

export default {
  data() {
    return {
      file: null,
      fileMd5: '',
      chunkSize: 2 * 1024 * 1024, // 2MB分片
      totalChunks: 0,
      uploadedChunks: new Set(),
      progress: 0
    };
  },
  methods: {
    async handleFileSelect(e) {
      this.file = e.target.files[0];
      this.totalChunks = Math.ceil(this.file.size / this.chunkSize);
      
      // 计算文件MD5(秒传关键)
      this.fileMd5 = await this.calculateMd5();
    },
    
    calculateMd5() {
      return new Promise((resolve) => {
        const blobSlice = File.prototype.slice;
        const chunks = this.totalChunks;
        const spark = new SparkMD5.ArrayBuffer();
        const fileReader = new FileReader();
        let currentChunk = 0;
        
        fileReader.onload = (e) => {
          spark.append(e.target.result);
          currentChunk++;
          
          if (currentChunk < chunks) {
            loadNext();
          } else {
            resolve(spark.end());
          }
        };
        
        const loadNext = () => {
          const start = currentChunk * this.chunkSize;
          const end = Math.min(start + this.chunkSize, this.file.size);
          fileReader.readAsArrayBuffer(blobSlice.call(this.file, start, end));
        };
        
        loadNext();
      });
    },
    
    async startUpload() {
      // 1. 秒传检查
      const { data } = await axios.post('/api/file/check', {
        fileMd5: this.fileMd5,
        chunkIndex: 0
      });
      
      if (data.exist && data.uploaded) {
        this.progress = 100;
        alert('秒传成功!');
        return;
      }
      
      // 2. 上传分片
      for (let i = 0; i < this.totalChunks; i++) {
        // 跳过已上传分片
        if (this.uploadedChunks.has(i)) continue;
        
        const chunk = this.file.slice(
          i * this.chunkSize,
          Math.min((i + 1) * this.chunkSize, this.file.size)
        );
        
        const formData = new FormData();
        formData.append('file', chunk);
        formData.append('fileMd5', this.fileMd5);
        formData.append('chunkIndex', i);
        
        await axios.post('/api/file/upload', formData, {
          headers: { 'Content-Type': 'multipart/form-data' }
        });
        
        // 更新进度
        this.uploadedChunks.add(i);
        this.progress = Math.round((this.uploadedChunks.size / this.totalChunks) * 100);
      }
      
      // 3. 合并请求
      await axios.post('/api/file/merge', {
        fileMd5: this.fileMd5,
        fileName: this.file.name,
        totalChunks: this.totalChunks
      });
      
      alert('上传完成!');
    }
  }
};
</script>
```
    
    
前端代码地址(麻烦各位大佬点个星星,在下感激不尽):
    https://github.com/xuxiaxuan/-/tree/master
 

http://www.xdnf.cn/news/13436.html

相关文章:

  • Linuxkernel学习-deepseek-2
  • Java-43 深入浅出 Nginx - 基本配置方式 nginx.conf Events块 HTTP块 反向代理 负载均衡
  • idea不同颜色总结
  • 【深尚想】LTR-390UV-01光宝环境光传感器电子元器件详细解析
  • HDFS 中 DataNode 挂载外部 S3 存储系统作为本地卷
  • 迁移科技3D视觉系统:开启袋子拆垛场景的智能革命新纪元
  • 53、错误处理-【源码分析】底层组件功能分析
  • Kafka消费者组位移重设指南
  • 从0到1掌握Sqoop:开启大数据迁移之旅
  • 爬取新浪新闻网的全部策略
  • 【kafka】rebalance机制详解
  • 基于GNU Radio Companion安装和搭建的简易FMRadio
  • Node.js版本管理
  • Contos7yum停服
  • latch/ff的电路结构及setup/hold/tpd、clkWidht/recovery/remove
  • Dexcap复现代码运行逻辑全流程(二)——realsense T265测试使用
  • 【学习笔记】RTSP-Ovnif-GB28181
  • vtk 对stl文件进行降采样
  • 鹰盾播放器AI识别字幕技术栈解析:从视频帧处理到语义理解的全流程实现
  • 工作总结及记录
  • vim的相关命令 + 三种模式(10)
  • Java异步编程难题
  • 保险丝的作用、基本参数和选型
  • vite原理
  • 智慧航空 | 飞机引擎设备拆解可视化
  • pysnmp模块中 GET、SET、WALK操作详细分步解析
  • 【Java】【力扣】121.买卖股票的最佳时机
  • 分布式I/O在食品包装行业中的应用
  • 239. 滑动窗口的最大值
  • [服务器] Amazon Lightsail SSH连接黑屏的常见原因及解决方案