惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
A
Arctic Wolf
S
Security Affairs
O
OpenAI News
SecWiki News
SecWiki News
TaoSecurity Blog
TaoSecurity Blog
H
Heimdal Security Blog
T
Threat Research - Cisco Blogs
Hacker News: Ask HN
Hacker News: Ask HN
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
C
Cisco Blogs
The Hacker News
The Hacker News
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
V
Vulnerabilities – Threatpost
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
T
Tenable Blog
T
The Exploit Database - CXSecurity.com
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Spread Privacy
Spread Privacy
人人都是产品经理
人人都是产品经理
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V2EX - 技术
V2EX - 技术
L
LINUX DO - 最新话题
The GitHub Blog
The GitHub Blog
博客园 - 三生石上(FineUI控件)
T
The Blog of Author Tim Ferriss
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Visual Studio Blog
The Cloudflare Blog
N
News and Events Feed by Topic
量子位
Google DeepMind News
Google DeepMind News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
L
LINUX DO - 热门话题
P
Palo Alto Networks Blog
Stack Overflow Blog
Stack Overflow Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Attack and Defense Labs
Attack and Defense Labs
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Hacker News - Newest:
Hacker News - Newest: "LLM"
Apple Machine Learning Research
Apple Machine Learning Research
The Register - Security
The Register - Security
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
Webroot Blog
Webroot Blog

博客园 - 漫思

python使用.env构建开发和生产环境 python项目的构建 nodejs构建CICD时的思考  成为 AI 智能体工程师的 10 个步骤 es6的 yield python 中的 yield 笔记本的A壳 thinkpad 更换 reduce() in Python python的字段赋值和取值的操作 python的语法类似于lodash分组展开,合并分组操作 SelectMany C# lodash 数组的常用做法 lodash里面的常用方法 技术的边界 Reduce 和 Transduce 的含义 尤雨溪创办的 VoidZero 官宣加入 Cloudflare,前端 Vite 等保持开源 Ramda 函数库参考教程 ramda es 数组的方法 flatmap map object.entity的教程 Map与FlatMap:在数据处理中的区别与联系 flat、flatmap与map的用法区别1 flat、flatmap与map的用法区别 AI不是从天而降,它经历了七十年三起三落:读懂AI的第三课 Agent 17 种架构模式 分析 & 思考 只有踩过坑才懂:前端生成唯一 ID,别用 Date.now ()了!试试它crypto.randomUUID() FastAPI python并发 代码是 AI 写的,生产事故谁背锅? AI Agent 走出 Demo 幻觉的唯一解药:Harness Engineering 高管的 AI 精神病 小米开源可控视频音效生成模型 ControlFoley ​IBM 联合红帽投资 50 亿美元:帮助企业确保开源软件安全 国家大基金领投 DeepSeek,首轮融资投前估值 450 亿美元 SpaceX 自研 AI 训练栈 V1.0 接近完工:用 C 重写、适配 22 万块 GB300 GPU 高管的 AI 精神病 - 漫思 做一款企业真正敢用的AI测试应用,到底有多难?究竟难在哪? - 漫思 工良吐槽篇:万字长文细说 AI 落地之笑谈 koarouter的路由基本能力 7. 简单语句 class-validator class- @koa/router 分路由 这是我见过的最好的koa2 classvalidate typeorm的能力 HLS.js destroy时会取消请求吗 从入门到精通:2025年本体管理工具选型指南,8款工具助你驾驭语义网络 一文入门智能体:dify 超快速构建AI agent C# 15 类型系统改进:Union Types skill网站 应无所住,而生其心 process.stdin.isTTY 谷歌学术走过风雨十年 听创始人畅谈苦辣酸甜 - 漫思 process.stdin.isTTY 是false的具体情况 asyncio 较为复杂的demo asyncio 简单demo python Annotated 独家解读:淘宝使用 Node.js 的 TypeScript 多场景开发和实践 mac uv 使用 Python 包管理工具 uv 使用教程 Python依赖管理新标杆:UV工具安装与实战指南 python的uv lobehubui python的enum通过int进行初始化 python的枚举类型 深度拆解:AI 智能体 Harness 的构造【译】 LlamaIndex是什么?LlamaIndex综述!看这一篇就够了! python和langchain的简单教程 nodejs的顶尖开源项目 基础Pydantic+TypedDict+Annotated +Dataclass 深度解析Python结构化数据工具:dataclass、Pydantic Model与TypedDict 被 LangChain 全家桶搞晕了?LangGraph、LangSmith、LangFlow 一文读懂 - 漫思 谁才是企业级开源平台的优选?OpenCSG与Dify、Coze、Langflow、Ollama 的差异化之路 四大框架全景解析:LangChain、LangGraph、DeepAgent、LangFlow 测试数据管理方式对比:dict vs dataclass vs TypedDict 革命性的写作:MDX 让你的 Markdown 全面动起来 Python - pydantic 入门介绍与 Models 的简单使用 欢迎使用 Pydantic¶ python模块导入 Python 里通常说“数组切片”,大多数时候指的是列表切片。基本格式是: 自定义hook的写法 Python 遍历字典的8种方法 python基础教程 or的简单用法 C# 15 引入了 联合类型 asyncio的基础理论 flask配置热更新 Form.Item 无法直接绑定两个 name Form.Item 的双向翻译官:getValueFromEvent 与 getValueProps Knex.js入门指南:Node.js最强大的SQL查询构建器 knex.js1 Knex.js RFC 9535:JSONPath 的标准化之路 C#多线程Thread.Join()的详解 Pretext:值得关注的文本排版引擎 Flask入门(四):Flask静态文件及配置 【高并发】消息队列思路 为何高并发系统中都要使用消息队列?这次彻底懂了! Node.js 消息队列应用:RabbitMQ、Kafka 与处理高并发任务 Node.js 缓存策略优化:Redis、Memcached 与本地缓存实现 .NET开发者的救星:8个让你告别996的高效库 别再吹牛了,100% Vibe Coding 存在无法自洽的逻辑漏洞!
大文件上传下载处理方案-断点续传,秒传,分片,合并
漫思 · 2026-04-09 · via 博客园 - 漫思

通常一些文件如视频文件体积都比较大,对于这些文件的上传需求要满足大文件的上传要求。http协议本身对上传文件大小没有限制,但是客户的网络环境质量、电脑硬件环境等参差不齐,如果一个大文件快上传完了网断了没有上传完成,需要客户重新上传,用户体验非常差,所以对于大文件上传的要求最基本的是断点续传。

引用百度百科:断点续传指的是在下载或上传时,将下载或上传任务(一个文件或一个压缩包)人为的划分为几个部分,每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载,断点续传可以提高节省操作时间,提高用户体验性。

大文件分片上传方案基于 “前端分片 + 后端合并 + 断点续传 + 秒传” 核心设计,完整流程分为 前端预处理 → 校验阶段 → 分片上传 → 后端合并 → 播放 / 下载 五个核心阶段,前后端分工明确:

import com.lgh.common.properties.MinIoProperties;
import com.lgh.web.service.VedioService;
import io.minio.*;
import io.minio.errors.MinioException;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import io.minio.messages.Item;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.compress.utils.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.util.List;
import java.util.ArrayList;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
 * 视频服务实现类
 * @Author GuihaoLv
 */
@Service
@Slf4j
public class VedioServiceImpl implements VedioService {
    @Autowired
    private MinioClient minioClient;
    @Autowired
    private MinIoProperties minIoProperties;
     /**
     * 文件上传前检查
     * @param fileMd5
     * @return
     */
    public Boolean checkFileExists(String fileMd5) {
        // 1. 构建完整文件在MinIO中的存储路径(与分块目录规则一致)
        String fullFilePath = getFullFilePath(fileMd5);
        try {
            // 2. 检查MinIO中是否存在该对象
            StatObjectArgs statArgs = StatObjectArgs.builder()
                    .bucket(minIoProperties.getBucketName())
                    .object(fullFilePath)
                    .build();
            minioClient.statObject(statArgs);
            log.info("文件已存在,MD5:{},MinIO路径:{}", fileMd5, fullFilePath);
            return true;
        } catch (MinioException e) {
            // MinIO返回对象不存在异常,说明文件未上传
            if (e.getMessage().equals("NoSuchKey")) {
                log.info("文件不存在,MD5:{}", fileMd5);
                return false;
            }
            // 其他异常打印日志,返回false
            log.error("检查文件存在性失败,MD5:{}", fileMd5, e);
            return false;
        } catch (Exception e) {
            log.error("检查文件存在性异常,MD5:{}", fileMd5, e);
            return false;
        }
    }
    /**
     * 检查分块是否已存在
     * 逻辑:拼接分块文件路径,检查MinIO中是否存在该分块对象
     */
    public Boolean checkChunkExists(String fileMd5, int chunk) {
        // 1. 构建分块文件在MinIO中的路径
        String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
        try {
            // 2. 检查分块对象是否存在
            StatObjectArgs statArgs = StatObjectArgs.builder()
                    .bucket(minIoProperties.getBucketName())
                    .object(chunkFilePath)
                    .build();
            minioClient.statObject(statArgs);
            log.info("分块已存在,MD5:{},分块索引:{}", fileMd5, chunk);
            return true;
        } catch (MinioException e) {
            if (e.getMessage().equals("NoSuchKey")) {
                log.info("分块不存在,MD5:{},分块索引:{}", fileMd5, chunk);
                return false;
            }
            log.error("检查分块存在性失败,MD5:{},分块索引:{}", fileMd5, chunk, e);
            return false;
        } catch (Exception e) {
            log.error("检查分块存在性异常,MD5:{},分块索引:{}", fileMd5, chunk, e);
            return false;
        }
    }
    /**
     * 上传分块文件到MinIO
     * 逻辑:MultipartFile转InputStream,上传到分块指定路径
     */
    public Boolean uploadChunk(MultipartFile file, String fileMd5, int chunk) {
        // 1. 校验参数
        if (file.isEmpty()) {
            log.error("上传分块失败,文件为空,MD5:{},分块索引:{}", fileMd5, chunk);
            return false;
        }
        // 2. 构建分块存储路径
        String chunkFilePath = getChunkFileFolderPath(fileMd5) + chunk;
        try (InputStream inputStream = file.getInputStream()) {
            // 3. 上传分块到MinIO
            PutObjectArgs putArgs = PutObjectArgs.builder()
                    .bucket(minIoProperties.getBucketName())
                    .object(chunkFilePath)
                    .stream(inputStream, file.getSize(), -1) // -1表示自动检测文件大小
                    .contentType(file.getContentType())
                    .build();
            minioClient.putObject(putArgs);
            log.info("分块上传成功,MD5:{},分块索引:{},路径:{}", fileMd5, chunk, chunkFilePath);
            return true;
        } catch (Exception e) {
            log.error("分块上传失败,MD5:{},分块索引:{}", fileMd5, chunk, e);
            return false;
        }
    }
    /**
     * 合并分块文件(优化版)
     * 核心逻辑:1. 有序构建分块列表 2. 合并文件 3. MD5校验 4. 清理分块
     * @param fileMd5 文件唯一标识(MD5)
     * @param fileName 原始文件名(含扩展名)
     * @param chunkTotal 分块总数(新增参数:避免遍历MinIO获取分块,提升性能)
     * @return 合并是否成功
     */
    public Boolean mergeChunk(String fileMd5, String fileName, int chunkTotal) {
        // 1. 基础路径构建
        String chunkFileFolderPath = getChunkFileFolderPath(fileMd5); //分块文件存储目录
        String extName = fileName.substring(fileName.lastIndexOf(".")); // 提取文件扩展名
        String mergeFilePath = getFilePathByMd5(fileMd5, extName); // 合并后文件路径
        try {
            // 2. 有序构建分块源列表(参考代码核心逻辑:按索引生成,保证顺序)
            List<ComposeSource> sourceObjectList = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> ComposeSource.builder()
                            .bucket(minIoProperties.getBucketName())
                            .object(chunkFileFolderPath.concat(Integer.toString(i)))
                            .build())
                    .collect(Collectors.toList());
            // 3. 执行MinIO分块合并
            ObjectWriteResponse response = minioClient.composeObject(
                    ComposeObjectArgs.builder()
                            .bucket(minIoProperties.getBucketName())
                            .object(mergeFilePath)
                            .sources(sourceObjectList)
                            .build());
            log.info("合并文件成功:{}", mergeFilePath);
            // 4. 下载合并后的文件,进行MD5校验(核心:保证文件完整性)
            File minioFile = downloadFileFromMinIO(minIoProperties.getBucketName(), mergeFilePath);
            if (minioFile == null) {
                log.error("下载合并后文件失败,mergeFilePath:{}", mergeFilePath);
                return false;
            }
            // 5. MD5校验逻辑
            try (InputStream newFileInputStream = new FileInputStream(minioFile)) {
                String md5Hex = DigestUtils.md5Hex(newFileInputStream);
                // 比对MD5,不一致则返回失败
                if (!fileMd5.equals(md5Hex)) {
                    log.error("文件合并校验失败,MD5不一致:原始{},合并后{}", fileMd5, md5Hex);
                    return false;
                }
                // 可选:此处可添加文件大小记录、入库等业务逻辑
                log.info("文件MD5校验通过,MD5:{}", fileMd5);
            }
            // 6. 清理分块文件(参考代码的清理逻辑,增加容错)
            clearChunkFiles(chunkFileFolderPath, chunkTotal);
            // 7. 临时文件删除(finally中兜底)
            return true;
        } catch (Exception e) {
            log.error("合并文件失败,fileMd5:{},异常:{}", fileMd5, e.getMessage(), e);
            return false;
        } finally {
            // 兜底:删除临时文件(若存在)
            File minioFile = new File(System.getProperty("java.io.tmpdir"), "minio.merge");
            if (minioFile.exists()) {
                minioFile.delete();
            }
        }
    }
    /**
     * 从MinIO下载文件到本地临时文件(参考代码的downloadFileFromMinIO)
     * @param bucket 桶名
     * @param objectName 对象路径
     * @return 本地临时文件
     */
    private File downloadFileFromMinIO(String bucket, String objectName) {
        File minioFile = null;
        FileOutputStream outputStream = null;
        try {
            // 从MinIO获取文件流
            InputStream stream = minioClient.getObject(GetObjectArgs.builder()
                    .bucket(bucket)
                    .object(objectName)
                    .build());
            // 创建临时文件
            minioFile = File.createTempFile("minio", ".merge");
            outputStream = new FileOutputStream(minioFile);
            IOUtils.copy(stream, outputStream); // 拷贝流到临时文件
            return minioFile;
        } catch (Exception e) {
            log.error("下载MinIO文件失败,bucket:{},object:{}", bucket, objectName, e);
            return null;
        } finally {
            // 关闭流
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    log.error("关闭文件输出流失败", e);
                }
            }
        }
    }
    /**
     * 清除分块文件(参考代码的clearChunkFiles,优化异常处理)
     * @param chunkFileFolderPath 分块文件目录
     * @param chunkTotal 分块总数
     */
    private void clearChunkFiles(String chunkFileFolderPath, int chunkTotal) {
        try {
            List<DeleteObject> deleteObjects = Stream.iterate(0, i -> ++i)
                    .limit(chunkTotal)
                    .map(i -> new DeleteObject(chunkFileFolderPath.concat(Integer.toString(i))))
                    .collect(Collectors.toList());
            RemoveObjectsArgs removeObjectsArgs = RemoveObjectsArgs.builder()
                    .bucket(minIoProperties.getBucketName())
                    .objects(deleteObjects)
                    .build();
            Iterable<Result<DeleteError>> results = minioClient.removeObjects(removeObjectsArgs);
            results.forEach(r -> {
                try {
                    DeleteError deleteError = r.get();
                    if (deleteError != null) {
                        log.error("清除分块文件失败,objectname:{}", deleteError.objectName());
                    }
                } catch (Exception e) {
                    log.error("遍历分块删除结果失败", e);
                }
            });
            log.info("分块文件清理完成,共处理{}个分块", chunkTotal);
        } catch (Exception e) {
            log.error("清除分块文件失败,chunkFileFolderPath:{}", chunkFileFolderPath, e);
        }
    }
    // ------------------- 私有工具方法 -------------------
    /**
     * 构建分块文件存储目录路径
     * 规则:md5前两位拆分目录 + md5 + chunk/
     * 示例:md5=abc123 → a/b/abc123/chunk/
     */
    private String getChunkFileFolderPath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/chunk/";
    }
    /**
     * 构建完整文件存储路径(不带文件名)
     */
    private String getFullFilePath(String fileMd5) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/";
    }
    /**
     * 构建完整文件存储路径(带文件名)
     */
    private String getFullFilePath(String fileMd5, String fileName) {
        return getFullFilePath(fileMd5) + fileName;
    }
    /**
     * 参考代码的getFilePathByMd5:构建合并后文件的完整路径
     * @param fileMd5 文件MD5
     * @param fileExt 文件扩展名(含.)
     * @return 完整路径
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }
}