Spring Boot 实现文件秒传功能

前言

在开发Web应用时,文件上传是一个常见需求。然而,当用户需要上传大文件或相同文件多次时,会造成带宽浪费和服务器存储冗余。此时可以使用文件秒传技术通过识别重复文件,实现瞬间完成上传的效果,大大提升了用户体验和系统效率。

文件秒传原理

文件秒传的核心原理是:

  1. 计算文件唯一标识(通常是MD5或SHA256值)
  2. 上传前先检查服务器是否已存在相同标识的文件
  3. 若存在,则直接引用已有文件,无需再次上传
  4. 若不存在,则执行常规上传流程

这种方式能显著减少网络传输避免存储冗余

代码实现

1. 创建项目基础结构

首先创建Spring Boot项目,添加必要依赖:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.18</version>
    </dependency>
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.1</version>
    </dependency>
</dependencies>

2. 创建上传存储代码

此处使用一个简单的集合来存储文件信息,实际使用需要替换为数据库或其他持久化中间件。

arduino 复制代码
import cn.hutool.crypto.digest.DigestUtil;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class FileService {
    
    // 使用Map存储文件信息,key为MD5,value为文件信息(实际使用时可替换为数据库存储)
    private final Map<String, FileInfo> fileStore = new ConcurrentHashMap<>();
    
    /**
     * 检查文件是否已存在
     */
    public FileInfo findByMd5(String md5) {
        return fileStore.get(md5);
    }
    
    /**
     * 保存文件信息
     */
    public FileInfo saveFile(String fileName, String fileMd5, Long fileSize, String filePath) {
        FileInfo fileInfo = new FileInfo(fileName, fileMd5, fileSize, filePath);
        fileStore.put(fileMd5, fileInfo); // 实际使用时插入数据库
        return fileInfo;
    }
    
    /**
     * 计算文件MD5
     */
    public String calculateMD5(MultipartFile file) throws IOException {
        return DigestUtil.md5Hex(file.getInputStream());
    }
}

定义一个简单的文件信息实体类:

typescript 复制代码
import cn.hutool.core.util.IdUtil;

public class FileInfo {

    private String id = IdUtil.fastUUID();
    private String fileName;
    private String fileMd5;
    private Long fileSize;
    private String filePath;

    public FileInfo(String fileName, String fileMd5, Long fileSize, String filePath) {
        this.fileName = fileName;
        this.fileMd5 = fileMd5;
        this.fileSize = fileSize;
        this.filePath = filePath;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFileMd5() {
        return fileMd5;
    }

    public void setFileMd5(String fileMd5) {
        this.fileMd5 = fileMd5;
    }

    public Long getFileSize() {
        return fileSize;
    }

    public void setFileSize(Long fileSize) {
        this.fileSize = fileSize;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }
}

3. 创建Result类

为了统一返回结果格式,可以创建一个简单的Result类。

typescript 复制代码
public class Result {
    private boolean success;
    private Object data;
    private String message;

    public Result(boolean success, Object data, String message) {
        this.success = success;
        this.data = data;
        this.message = message;
    }

    public static Result success(Object data) {
        return new Result(true, data,"success");
    }

    public static Result success(Object data,String message) {
        return new Result(true, data,message);
    }

    public static Result error(String message) {
        return new Result(false, null, message);
    }

    // Getters
    public boolean isSuccess() { return success; }
    public Object getData() { return data; }
    public String getMessage() { return message; }
}

4. 创建Controller控制器

kotlin 复制代码
import cn.hutool.core.io.FileUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;

@RestController
@RequestMapping("/api/file")
public class FileController {

    private static Logger logger = LoggerFactory.getLogger(FileController.class);

    @Autowired
    private FileService fileService;

    /**
     * 检查文件是否已存在
     */
    @PostMapping("/check")
    public Result checkFile(@RequestParam("md5") String md5) {
        FileInfo fileInfo = fileService.findByMd5(md5);
        if (fileInfo != null) {
            return Result.success(fileInfo);
        }
        return Result.success(null);
    }
    
    /**
     * 上传文件
     */
    @PostMapping("/upload")
    public Result uploadFile(@RequestParam("file") MultipartFile file) {
        try {
            // 计算文件MD5值
            String md5 = fileService.calculateMD5(file);
            
            // 检查文件是否已存在
            FileInfo existFile = fileService.findByMd5(md5);
            if (existFile != null) {
                // todo 进行自定义的逻辑处理
                return Result.success(existFile,"文件秒传成功");
            }
            
            // 文件不存在,执行上传
            String originalFilename = file.getOriginalFilename();
            String filePath = FileUtil.getTmpDir() + File.separator + originalFilename; // 保存到临时目录
            
            // 存储文件
            file.transferTo(new File(filePath));
            
            // 保存文件信息到内存(实际使用时应替换为数据库)
            FileInfo fileInfo = fileService.saveFile(originalFilename, md5, file.getSize(), filePath);
            return Result.success(fileInfo,"文件上传成功");
        } catch (Exception e) {
            logger.error(e.getMessage(),e);
            return Result.error("文件上传失败:" + e.getMessage());
        }
    }
}

4. 创建纯HTML前端页面

创建一个简单的HTML上传页面:

xml 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <title>文件秒传示例</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/spark-md5@3.0.2/spark-md5.min.js"></script>
</head>
<body>
    <h2>文件上传(支持秒传)</h2>
    <input type="file" id="fileInput" />
    <button onclick="uploadFile()">上传文件</button>
    <div id="progressBar" style="display:none;">
        <div>上传进度:<span id="progress">0%</span></div>
    </div>
    <div id="result"></div>
    <script>
        function uploadFile() {
            const fileInput = document.getElementById('fileInput');
            const file = fileInput.files[0];
            if (!file) {
                alert('请选择文件');
                return;
            }
            
            document.getElementById('progressBar').style.display = 'block';
            document.getElementById('result').innerText = '计算文件MD5中...';

            // 计算文件MD5
            calculateMD5(file).then(md5 => {
                document.getElementById('result').innerText = '正在检查文件是否已存在...';
                
                // 检查文件是否已存在
                return axios.post('/api/file/check', {
                    md5: md5
                }).then(response => {
                    if (response.data.data && response.data.data.id) {
                        // 文件已存在,执行秒传
                        document.getElementById('result').innerText = '文件秒传成功!';
                        document.getElementById('progress').innerText = '100%';
                        return Promise.resolve();
                    } else {
                        // 文件不存在,执行上传
                        const formData = new FormData();
                        formData.append('file', file);
                        
                        return axios.post('/api/file/upload', formData, {
                            onUploadProgress: progressEvent => {
                                const percentCompleted = Math.round(
                                    (progressEvent.loaded * 100) / progressEvent.total
                                );
                                document.getElementById('progress').innerText = percentCompleted + '%';
                            }
                        }).then(response => {
                            document.getElementById('result').innerText = '文件上传成功!';
                        });
                    }
                });
            }).catch(error => {
                document.getElementById('result').innerText = '错误:' + error.message;
            });
        }
        
        // 计算文件MD5
        function calculateMD5(file) {
            return new Promise((resolve, reject) => {
                const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
                const chunkSize = 2097152; // 2MB
                const chunks = Math.ceil(file.size / chunkSize);
                let currentChunk = 0;
                const spark = new SparkMD5.ArrayBuffer();
                const fileReader = new FileReader();
                
                fileReader.onload = function(e) {
                    spark.append(e.target.result);
                    currentChunk++;
                    
                    if (currentChunk < chunks) {
                        loadNext();
                    } else {
                        resolve(spark.end());
                    }
                };
                
                fileReader.onerror = function() {
                    reject('文件读取错误');
                };
                
                function loadNext() {
                    const start = currentChunk * chunkSize;
                    const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;
                    fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
                }
                
                loadNext();
            });
        }
    </script>
</body>
</html>

5. 配置文件

application.yml中添加必要配置

yaml 复制代码
server:
  port: 8080

spring:
  servlet:
    multipart:
      max-file-size: 100MB
      max-request-size: 100MB

效果

第一次上传
第二次上传
相关推荐
hqxstudying26 分钟前
java依赖注入方法
java·spring·log4j·ioc·依赖
·云扬·34 分钟前
【Java源码阅读系列37】深度解读Java BufferedReader 源码
java·开发语言
martinzh2 小时前
Spring AI 项目介绍
后端
Bug退退退1232 小时前
RabbitMQ 高级特性之重试机制
java·分布式·spring·rabbitmq
小皮侠2 小时前
nginx的使用
java·运维·服务器·前端·git·nginx·github
前端付豪2 小时前
20、用 Python + API 打造终端天气预报工具(支持城市查询、天气图标、美化输出🧊
后端·python
爱学习的小学渣2 小时前
关系型数据库
后端
武子康2 小时前
大数据-33 HBase 整体架构 HMaster HRegion
大数据·后端·hbase
前端付豪2 小时前
19、用 Python + OpenAI 构建一个命令行 AI 问答助手
后端·python
凌览2 小时前
斩获 27k Star,一款开源的网站统计工具
前端·javascript·后端