简介这是一份面向Python Web开发者的Flask大文件分片上传与流式下载演示项目适合想学习前后端配合处理大文件传输、进度条渲染及多用户并发上传的初中级开发者。资源包共26个文件约427KB包含2个Python后端脚本、2个HTML页面模板、5个CSS与5个JS前端资源以及字体、图标、依赖清单和说明文档等结构清晰便于直接运行与二次开发。项目基于Flask框架前端借助WebUploader实现分片上传与Bootstrap进度条展示后端接收全部分片后合并为完整文件并支持流式下载以降低内存占用同时允许多用户同时上传互不干扰。目前已有651人学习下载。通过阅读源码与运行示例读者可掌握分片上传、文件合并、流式响应等关键实现思路并了解后续优化方向如避免合并时内存紧张、按序回传分片等是学习大文件传输方案的实用参考。1. 大文件上传总超时聊聊 Flask WebUploader 的分段上传方案做过后台管理系统的同行大概率都遇到过这个场景客户要传一个 2GB 的培训视频或者一整套设计源文件前端进度条卡在 99% 不动后端 Nginx 直接甩回一个 413 Request Entity Too Large或者 Flask 开发服务器默默把连接断掉。传统表单上传在这个量级下基本就是玄学网络一抖全部重来用户体验和服务器内存双双爆炸。upload-demo这个资源包给出的思路很直接用 Flask 做后端接收用 WebUploader 做前端分片调度把一个大文件切成若干固定大小的块逐块上传、逐块落盘最后在服务端合并。它解决的不是能不能传的问题而是传得稳、传得可恢复、传得不吃内存的问题。适合正在做文件管理系统、在线教育资料库、企业内部文档平台的 Python 后端和前端开发者尤其是那些被大文件上传反复折磨过的人。2. 分段上传的核心机制分片、校验与合并的完整链路2.1 为什么不能一次性把文件读进内存很多人第一版实现是这样的前端FormData塞整个文件后端request.files[file].read()一把梭。文件小的时候没问题一旦超过几百 MBFlask 默认会把请求体缓存在内存或临时文件里MAX_CONTENT_LENGTH没设的话直接撑爆 worker。更麻烦的是这种模式下只要网络中断一次整个上传就废了没有断点续传的可能。分段上传的本质是把一次大事务拆成多次小事务。每个分片独立发送、独立确认服务端只需要维护一个分片状态表。常见做法是前端按固定大小切片比如每片 2MB给每个分片带上chunkIndex、totalChunks、fileHash三个关键标识。后端收到分片后先存到临时目录等所有分片到齐再按顺序拼接。这样做的好处是单次请求体积可控内存占用恒定某个分片失败只需重传那一片服务端可以在合并前做完整性校验。WebUploader 在这套流程里扮演的是调度器角色。它内置了分片、并发控制、重试机制通过chunked和threads两个配置就能开启。相比自己用File.slice()手写WebUploader 帮你处理了并发队列和失败重试的边界情况省了不少事。2.2 前端分片配置WebUploader 的关键参数先看前端初始化部分。WebUploader 的配置项比较多但真正影响分段上传的就这么几个配错了要么不分片要么分片顺序乱掉。// 初始化 WebUploader 实例 var uploader WebUploader.create({ // 后端接收分片的接口地址 server: /api/upload/chunk, // 指定使用 HTML5 模式Flash 模式不支持分片 swf: /static/Uploader.swf, // 选择文件后自动上传 auto: true, // 开启分片上传 chunked: true, // 每片大小 2MB注意单位是字节 chunkSize: 2 * 1024 * 1024, // 并发上传线程数建议 3-5太高会被浏览器限制 threads: 3, // 单文件最大限制这里设 10GB fileSingleSizeLimit: 10 * 1024 * 1024 * 1024, // 只允许特定类型 accept: { title: Files, extensions: zip,rar,7z,mp4,iso, mimeTypes: .zip,.rar,.7z,.mp4,.iso }, // 分片上传时携带额外参数 formData: { // 业务侧的用户标识用于隔离不同用户的上传目录 userId: current_user_id } });这里有几个参数值得展开说。chunkSize设 2MB 是个经验值太小会导致请求数暴增Nginx 日志刷得飞快太大则失去分片的意义单次失败重传成本高。threads设 3 到 5 之间比较稳妥浏览器对同域名并发连接数有限制设太高反而会排队。fileSingleSizeLimit必须设否则用户选了个 50GB 的镜像文件前端切片阶段就可能卡死。WebUploader 在分片模式下会自动给每个分片附加chunk、chunks、id等参数后端靠这些字段来识别分片归属。你不需要手动传chunkIndex框架已经帮你做了。2.3 后端接收分片Flask 路由与临时存储后端这块是整个方案的核心。Flask 接收分片的逻辑不复杂但要处理好目录结构和状态记录。import os import hashlib from flask import Flask, request, jsonify app Flask(__name__) # 分片临时存储根目录 CHUNK_ROOT /data/upload_chunks # 最终合并文件存放目录 FINAL_ROOT /data/upload_final app.route(/api/upload/chunk, methods[POST]) def receive_chunk(): # WebUploader 自动携带的分片元信息 chunk_index int(request.form.get(chunk, 0)) total_chunks int(request.form.get(chunks, 1)) file_id request.form.get(id) # 本次上传的唯一标识 file_name request.form.get(name) user_id request.form.get(userId, anonymous) # 每个上传任务独立一个临时目录避免不同文件分片混淆 task_dir os.path.join(CHUNK_ROOT, user_id, file_id) os.makedirs(task_dir, exist_okTrue) # 分片文件以序号命名合并时按序号排序 chunk_file request.files.get(file) chunk_path os.path.join(task_dir, f{chunk_index:06d}.part) chunk_file.save(chunk_path) # 检查是否所有分片都已到达 received len([f for f in os.listdir(task_dir) if f.endswith(.part)]) if received total_chunks: # 触发合并 final_path merge_chunks(task_dir, FINAL_ROOT, file_name, user_id) return jsonify({code: 0, msg: upload_complete, path: final_path}) return jsonify({code: 0, msg: chunk_received, received: received})这段代码有几个设计点。第一用user_id和file_id两级目录隔离防止不同用户或不同文件的分片互相覆盖。第二分片文件名用{:06d}格式化保证按字符串排序时顺序正确否则10.part会排在2.part前面合并出来就是坏文件。第三每次收到分片都检查目录内文件数量等于total_chunks时触发合并。这个判断在高并发下可能有竞态生产环境建议用 Redis 计数器或者数据库状态表来替代。2.4 合并分片顺序拼接与完整性校验合并逻辑本身不复杂但有两个坑一是必须按序号顺序写入二是合并后要校验文件完整性。def merge_chunks(task_dir, final_root, file_name, user_id): # 按文件名排序确保分片顺序正确 chunk_files sorted([ os.path.join(task_dir, f) for f in os.listdir(task_dir) if f.endswith(.part) ]) # 最终文件路径加时间戳防止同名覆盖 import time safe_name f{int(time.time())}_{file_name} final_dir os.path.join(final_root, user_id) os.makedirs(final_dir, exist_okTrue) final_path os.path.join(final_dir, safe_name) # 流式写入避免把整个文件读进内存 with open(final_path, wb) as out: for chunk_path in chunk_files: with open(chunk_path, rb) as cf: # 每次读 1MB内存占用恒定 while True: buf cf.read(1024 * 1024) if not buf: break out.write(buf) # 合并完成后清理临时分片 for chunk_path in chunk_files: os.remove(chunk_path) os.rmdir(task_dir) return final_path合并时用read(1024 * 1024)循环写入而不是out.write(cf.read())是为了控制内存峰值。一个 10GB 的文件如果一次性读入Python 进程直接 OOM。分片清理放在合并成功之后万一合并中途失败分片还在可以重试。提示生产环境建议在合并完成后计算一次文件的 MD5 或 SHA256和前端上传前计算的哈希值比对确认文件没有在传输过程中损坏。3. 避坑指南分段上传最容易翻车的五个地方3.1 分片顺序错乱导致文件损坏现象上传进度显示 100%但合并后的文件打不开视频花屏、压缩包解压报错。原因分片文件名没有做零填充1.part、10.part、2.part按字符串排序变成1, 10, 2合并顺序完全乱了。或者前端并发上传时后端没有按chunkIndex命名而是用了随机 ID。解决分片文件名统一用固定位数格式化比如{:06d}.part。合并前打印一下排序后的文件列表确认顺序是000000, 000001, 000002...。如果还是不放心可以在合并时按chunkIndex显式排序而不是依赖文件名。3.2 Nginx 的 client_max_body_size 没调现象小分片能传上去稍微大一点的分片直接返回 413前端控制台看到Request Entity Too Large。原因Nginx 默认client_max_body_size是 1MB虽然后端 Flask 设了MAX_CONTENT_LENGTH但请求还没到 Flask 就被 Nginx 拦了。解决在 Nginx 配置的server或location块里加上client_max_body_size 10m;要比chunkSize略大。改完记得nginx -s reload。如果用了云厂商的负载均衡那一层也要检查请求体限制。3.3 并发上传时临时目录冲突现象两个用户同时上传同名文件合并出来的文件内容混在一起或者分片数量对不上。原因临时目录只用文件名做标识没有加用户 ID 或上传会话 ID不同任务的分片写到了同一个目录。解决临时目录路径必须包含唯一标识推荐CHUNK_ROOT/user_id/file_id/三级结构。file_id用 WebUploader 生成的id字段每次选择文件都是唯一的。不要用文件名做目录名同名文件太常见了。3.4 合并后临时分片没清理磁盘爆满现象系统跑了一段时间磁盘使用率飙升排查发现/data/upload_chunks目录下堆了几百 GB 的.part文件。原因合并成功后忘记删除分片或者合并失败后没有清理机制失败任务的分片一直留着。解决合并成功后立即删除分片目录。另外加一个定时清理任务扫描超过 24 小时未合并的临时目录自动删除。可以用find /data/upload_chunks -name *.part -mtime 1 -delete配合 cron 做兜底。3.5 前端计算文件哈希阻塞 UI现象用户选了文件后页面卡死几秒到几十秒大文件尤其明显。原因为了做秒传或完整性校验前端在选文件后立即计算整个文件的 MD5主线程被占满。解决用 Web Worker 在后台线程计算哈希或者只计算文件头尾各 1MB 的抽样哈希。如果业务不需要秒传干脆跳过前端哈希只靠后端合并后校验。WebUploader 本身不强制要求文件哈希这是业务层自己加的。4. 进阶技巧断点续传与秒传的落地细节4.1 断点续传让用户刷新页面后还能接着传分段上传天然支持断点续传但需要后端提供一个查询已上传分片的接口。前端在开始上传前先问后端这个文件已经传了哪些分片然后只传缺失的那些。app.route(/api/upload/check, methods[POST]) def check_chunks(): file_id request.form.get(id) user_id request.form.get(userId, anonymous) task_dir os.path.join(CHUNK_ROOT, user_id, file_id) if not os.path.exists(task_dir): return jsonify({code: 0, uploaded: []}) # 返回已上传的分片序号列表 uploaded [ int(f.replace(.part, )) for f in os.listdir(task_dir) if f.endswith(.part) ] return jsonify({code: 0, uploaded: uploaded})前端拿到uploaded列表后在 WebUploader 的uploadBeforeSend钩子里跳过已存在的分片。不过 WebUploader 原生不支持跳过指定分片常见做法是拦截uploadAccept或者在beforeSend里返回false来取消该分片。更干净的方式是换用支持断点续传的库或者自己在 WebUploader 外面包一层调度逻辑。4.2 秒传文件哈希命中直接返回秒传的核心是文件级哈希。前端选文件后计算整个文件的 SHA256先请求后端查这个哈希是否已存在。存在就直接返回文件路径不需要上传任何分片。import hashlib def calc_file_hash(file_path): sha hashlib.sha256() with open(file_path, rb) as f: while True: buf f.read(1024 * 1024) if not buf: break sha.update(buf) return sha.hexdigest()后端维护一张file_hash - file_path的映射表可以用数据库或者 Redis。秒传的命中率取决于业务场景如果是企业内部文档库同一份文件被多人上传的概率很高秒传能省大量带宽。但如果是用户个人相册基本不会命中反而多了一次哈希计算的开销。注意秒传的哈希计算建议放在 Web Worker 里并且只对小于 500MB 的文件启用。超大文件计算 SHA256 本身就要几十秒用户体验反而更差。4.3 分片大小怎么选一张参数对照表文件大小范围推荐分片大小并发数理由 50MB不分片1分片开销大于收益50MB - 500MB1MB3平衡请求数和单次失败成本500MB - 2GB2MB3减少请求数降低 Nginx 压力2GB - 10GB5MB2大分片减少总请求数并发降低避免超时 10GB10MB2必须配合断点续传否则失败成本太高这张表是经验值实际要根据服务器带宽和用户网络环境调整。我一般会在测试环境用不同分片大小跑一遍看总上传时间和失败率选一个折中点。4.4 验证上传结果别只看进度条进度条到 100% 不代表文件真的传完了。WebUploader 的uploadSuccess事件只表示所有分片都收到了 HTTP 200 响应但合并可能失败或者合并后的文件损坏。我习惯在合并完成后做一次校验后端返回最终文件的 MD5前端拿到后和本地计算的哈希比对。不一致就提示用户重新上传。uploader.on(uploadSuccess, function(file, response) { if (response.msg upload_complete) { // 后端返回合并后文件的哈希 var serverHash response.fileHash; // 前端本地计算的哈希需提前在 Worker 中算好 if (serverHash ! localHash) { alert(文件校验失败请重新上传); return; } console.log(上传成功文件路径, response.path); } });从那以后我每次做上传功能都会强制走一遍分片顺序检查 合并后哈希比对 临时目录清理这三步少一步都可能在上线后收到用户投诉。希望这套思路能帮到你少踩几个我踩过的坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?