Spring Boot 实现文件秒传功能

news2025/4/8 1:47:09

前言

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

文件秒传原理

文件秒传的核心原理是:

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

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

代码实现

1. 创建项目基础结构

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

<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. 创建上传存储代码

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

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());
    }
}

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

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类。

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控制器

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上传页面:

<!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中添加必要配置

server:
  port: 8080

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

效果

第一次上传

在这里插入图片描述

第二次上传

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2326958.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用AOP技术实现Java通用接口验签工具

一、背景 在给第三方提供接口时,我们需要对接口进行验签。具体来说,当外部系统调用我们的接口时,请求中需要携带一个签名,我们接收到请求后,会解析数据并校验签名是否正确,以确保请求的合法性和安全性。 为了在不同项目中方便地使用这一功能,我们将签名校验规则封装成一…

aarch64-none-elf-gcc与aarch64-linux-gnu-gcc

1. 场景描述 在Ubuntu 24.04.1 LTS x86_64架构下交叉编译能跑在aarch64架构下裸机程序&#xff0c;遇到缺aarch64-none-elf-gcc的情况&#xff0c;做此记录。 2. aarch64-none-elf-gcc与aarch64-linux-gnu-gcc 运行环境 aarch64-none-elf-gcc 生成的代码是 裸机程序&#xf…

【清华大学】DeepSeek政务应用场景与解决方案

目录 一、政务数字化转型三阶段演进二、人工智能政务应用场景四大方向 三、技术方案核心技术 四、解决方案案例1. 公文写作2. 合同协议智能审查3. 行政执法4. 就业指导 五、风险及对策六、落地大四步法七、未来发展展望AI职业替代逻辑空间智能与具身智能人机共生 一、政务数字化…

4.2 单相机引导机器人放料-仅考虑角度变化

【案例说明】 本案例产品在托盘中,角度变化不大(<15度);抓取没有问题,只是放的穴位只能容许3度的角度偏差,因此需要测量产品的角度。 思路是:机器人抓料后、去固定拍照位拍照(找到与标准照片的角度偏差),机器人在放料的位置上多旋转这个角度偏差,把产品放进去。 …

论文阅读笔记:Denoising Diffusion Implicit Models (3)

0、快速访问 论文阅读笔记&#xff1a;Denoising Diffusion Implicit Models &#xff08;1&#xff09; 论文阅读笔记&#xff1a;Denoising Diffusion Implicit Models &#xff08;2&#xff09; 论文阅读笔记&#xff1a;Denoising Diffusion Implicit Models &#xff08…

Git(八)如何在同一台电脑登录两个Git

目录 一、理解 SSH 密钥机制二、具体实现步骤1.删除GIT全局配置2.生成多个 SSH 密钥3.添加公钥到 Git 账户4.配置 SSH config 文件5.测试SSH key是否生效6.下载代码 三、Git仓库级别配置四、HTTPS方式的多账号管理 引言&#xff1a; 在日常开发中&#xff0c;我们经常会遇到需要…

如何改电脑网络ip地址:一步步指导

有时我们需要更改电脑的网络IP地址以满足特定的网络需求。本文将为您提供一份详细的步骤指南&#xff0c;帮助您轻松完成电脑网络IP地址的更改。以下是更改计算机IP地址的分步指南&#xff0c;适用于常见的操作系统&#xff1a; 一、更换内网ip Windows 系统&#xff08;Win10…

PyTorch 分布式训练(Distributed Data Parallel, DDP)简介

PyTorch 分布式训练&#xff08;Distributed Data Parallel, DDP&#xff09; 一、DDP 核心概念 torch.nn.parallel.DistributedDataParallel 1. DDP 是什么&#xff1f; Distributed Data Parallel (DDP) 是 PyTorch 提供的分布式训练接口&#xff0c;DistributedDataPara…

【Unity】记录TMPro使用过程踩的一些坑

1、打包到webgl无法输入中文&#xff0c;编辑器模式可以&#xff0c;但是webgl不行&#xff0c;试过网上的脚本&#xff0c;还是不行 解决方法&#xff1a;暂时没找到 2、针对字体asset是中文时&#xff0c;overflow的效果模式处理奇怪&#xff0c;它会出现除了overflow模式以…

计算机视觉初步(环境搭建)

1.anaconda 建议安装在D盘&#xff0c;官网正常安装即可&#xff0c;一般可以安装windows版本 安装成功后&#xff0c;可以在电脑应用里找到&#xff1a; 2.创建虚拟环境 打开anaconda prompt&#xff0c; 可以用conda env list 查看现有的环境&#xff0c;一般打开默认bas…

基于聚类与引力斥力优化的选址算法

在众多实际场景中&#xff0c;诸如消防设施选址、基站布局规划以及充电桩站点部署等&#xff0c;都面临着如何利用最少的资源&#xff0c;实现对所有目标对象全面覆盖的难题。为有效解决这类问题&#xff0c;本文提出一种全新的组合算法模型 —— 基于聚类与引力斥力优化的选址…

Mac 电脑移动硬盘无法识别的解决方法

在使用 Mac 电脑的过程中&#xff0c;不少用户都遇到过移动硬盘没有正常推出&#xff0c;导致无法识别的问题。这不仅影响了数据的传输&#xff0c;还可能让人担心硬盘内数据的安全。今天&#xff0c;我们就来详细探讨一下针对这一问题的解决方法。 当发现移动硬盘无法识别时&…

LeetCode Hot100 刷题笔记(4)—— 二叉树、图论

目录 一、二叉树 1. 二叉树的深度遍历&#xff08;DFS&#xff1a;前序、中序、后序遍历&#xff09; 2. 二叉树的最大深度 3. 翻转二叉树 4. 对称二叉树 5. 二叉树的直径 6. 二叉树的层序遍历 7. 将有序数组转换为二叉搜索树 8. 验证二叉搜索树 9. 二叉搜索树中第 K 小的元素 …

【计算机视觉】YOLO语义分割

一、语义分割简介 1. 定义 语义分割&#xff08;Semantic Segmentation&#xff09;是计算机视觉中的一项任务&#xff0c;其目标是对图像中的每一个像素赋予一个类别标签。与目标检测只给出目标的边界框不同&#xff0c;语义分割能够在像素级别上区分不同类别&#xff0c;从…

【SpringBoot + MyBatis + MySQL + Thymeleaf 的使用】

目录&#xff1a; 一&#xff1a;创建项目二&#xff1a;修改目录三&#xff1a;添加配置四&#xff1a;创建数据表五&#xff1a;创建实体类六&#xff1a;创建数据接口七&#xff1a;编写xml文件八&#xff1a;单元测试九&#xff1a;编写服务层十&#xff1a;编写控制层十一…

在ensp进行OSPF+RIP+静态网络架构配置

一、实验目的 1.Ospf与RIP的双向引入路由消息 2.Ospf引入静态路由信息 二、实验要求 需求&#xff1a; 路由器可以互相ping通 实验设备&#xff1a; 路由器router7台 使用ensp搭建实验坏境&#xff0c;结构如图所示 三、实验内容 1.配置R1、R2、R3路由器使用Ospf动态路由…

Redis安全与配置问题——AOF文件损坏问题及解决方案

Java 中的 Redis AOF 文件损坏问题全面解析 一、AOF 文件损坏的本质与危害 1.1 AOF 持久化原理 Redis 的 AOF&#xff08;Append-Only File&#xff09; 通过记录所有写操作命令实现持久化。文件格式如下&#xff1a; *2\r\n$6\r\nSELECT\r\n$1\r\n0\r\n *3\r\n$3\r\nSET\r\…

3.第二阶段x64游戏实战-分析人物移动实现人物加速

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 上一个内容&#xff1a;2.第二阶段x64游戏实战-x64dbg的使用 想找人物的速度&#xff0c;就需要使用Ch…

Scala(三)

本节课学习了函数式编程&#xff0c;了解到它与Java、C函数式编程的区别&#xff1b;学习了函数的基础&#xff0c;了解到它的基本语法、函数和方法的定义、函数高级。。。学习到函数至简原则&#xff0c;高阶函数&#xff0c;匿名函数等。 函数的定义 函数基本语法 例子&…

什么是 Java 泛型

一、什么是 Java 泛型&#xff1f; 泛型&#xff08;Generics&#xff09; 是 Java 中一种强大的编程机制&#xff0c;允许在定义类、接口和方法时使用类型参数。通过泛型&#xff0c;可以将数据类型作为参数传递&#xff0c;从而实现代码的通用性和类型安全。 简单来说&…