ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Flask分片上传实战:大文件断点续传与流式写入

Flask分片上传实战:大文件断点续传与流式写入 简介这是一份基于Flask与WebUploader实现的大文件分片上传与流式下载的完整Web开发示例面向Python后端初学者、Web全栈学习者及需要解决大文件传输瓶颈的开发者。资源通过前端分片上传、服务端分片接收与合并、多用户隔离存储、内存友好的流式响应等机制切实应对传统单次HTTP上传易超时、OOM、并发冲突等典型问题。压缩包共26个文件含5个CSS与5个JS文件支撑Bootstrap界面与WebUploader交互2个核心Python脚本server.py与upload/init.py实现Flask路由与分片逻辑4个map文件辅助调试2个HTML页面提供上传入口与文件列表另有字体、图标及requirements依赖清单整体仅427KB轻量易部署。已有651人学习下载读者可直接运行体验分片上传成功/失败状态、实时进度条渲染、服务端分片合并流程及低内存占用的流式下载效果同时获得符合BSD/MIT协议规范的开源集成实践参考。1. 分段上传不是“把大文件切成小块”就完事upload-demo 是一套能扛住 2GB 视频、断点续传不丢数据、前端拖拽后端流式写入的 Flask 实战样板你试过用普通input typefile上传一个 1.8GB 的监控录像吗浏览器卡死、504 网关超时、Flask 默认 16MB 请求体限制直接报错——这不是配置调得不够狠而是架构错了。upload-demo 就是为这种真实场景而生它用 WebUploader 做前端分片控制支持拖拽、暂停、续传、进度条用 Flask 做轻量后端不依赖 Celery、不硬编码路径、不写死文件名核心逻辑是「接收分片 → 合并校验 → 流式落盘」。它不是玩具 demo而是我在线上教育平台部署时抄过的底板——学生上传 4K 课程视频、教师批量导入 300 个 PPT 附件都靠它稳住。适合正在用 Flask 开发内容管理后台、教学系统、医疗影像归档模块的工程师不适合纯静态页面或已用 FastAPI 做高并发上传的团队。关键词里那个stream-downloader其实是误标真正关键的是fragmentation分片和BootstrapHTML它用 Bootstrap 4 快速搭出响应式上传界面不是花哨 UI而是能适配 iPad 和老旧 IE11 的实用布局。2. 为什么选 WebUploader 而不是原生 Fetch Blob.slice——从协议层看分片上传的不可替代性2.1 分片上传的本质绕过 HTTP 协议瓶颈把「传输可靠性」从 TCP 层移到应用层HTTP/1.1 本身不支持断点续传。你用fetch()发一个 2GB 的 Blob中间网络抖动一次整个请求就失败重试就得从头传。WebUploader 的分片机制本质是把一个大文件切成固定大小如 5MB的 chunk每个 chunk 单独发 POST 请求带唯一标识chunkIndex、totalChunks、fileMd5。服务端收到后先校验fileMd5 chunkIndex是否已存在再决定是跳过、覆盖还是追加。这相当于在应用层实现了「TCP 的 ACK 机制」丢一个 chunk只重传那一个不影响其他。Flask 本身没提供分片 API但它的 request.stream 和 werkzeug.utils.secure_filename 让我们能安全地接收、命名、拼接这些 chunk——这才是 upload-demo 的技术锚点不是炫技是刚需。2.2 Flask 端为何不用request.files流式接收才是处理大文件的唯一正解很多新手一上来就写request.files[file]这在分片上传里是致命错误。request.files会把整个 multipart body 加载进内存2GB 文件直接 OOM。upload-demo 的核心是request.stream# app.py 关键片段 app.route(/upload, methods[POST]) def upload_chunk(): # 不用 request.files用 stream 直接读原始字节流 chunk_data request.stream.read() # 读取当前分片全部内容 file_md5 request.form.get(md5) chunk_index int(request.form.get(chunkIndex)) total_chunks int(request.form.get(totalChunks)) # 安全构造临时路径避免 ../ 路径遍历 temp_dir os.path.join(app.config[UPLOAD_FOLDER], chunks, file_md5) os.makedirs(temp_dir, exist_okTrue) chunk_path os.path.join(temp_dir, f{chunk_index:05d}) # 二进制写入不经过内存缓冲 with open(chunk_path, wb) as f: f.write(chunk_data) return jsonify({status: success, chunk: chunk_index})提示request.stream.read()会阻塞直到客户端发完当前 chunk但不会把整个文件加载进 Python 进程内存。这是 Werkzeug 底层用wsgi.input直接读取 socket buffer 的结果比request.get_data()安全十倍。2.3 WebUploader 前端如何与 Flask 后端对齐分片参数四个必须同步的字段WebUploader 的server配置项必须和 Flask 路由严格匹配否则分片会 400。以下是实际调试中验证过的最小可行配置// static/js/upload.js var uploader WebUploader.create({ // 1. 分片大小后端合并时按此切分建议 2-10MB太小增加 HTTP 开销太大失去断点意义 chunkSize: 5 * 1024 * 1024, // 5MB // 2. 并发数同时上传几个分片Flask 单进程默认 1设为 1 最稳若用 gunicorn 多 worker可设 3 threads: 1, // 3. 分片校验必须开启否则无法判断重复上传或损坏 chunkRetry: 2, // 失败重试次数 md5: true, // 启用文件级 MD5WebUploader 自动计算 // 4. 后端接口注意 query 参数必须和 Flask request.form.get() 一致 server: /upload, formData: { md5: , // WebUploader 自动填入 chunkIndex: , // 自动填入 totalChunks: // 自动填入 } });关键点在于formData里的 key 名必须和 Flask 中request.form.get(xxx)完全一致。我曾因把chunkIndex写成chunk_index导致后端收不到索引所有分片都存到00000文件里——合并时顺序全乱。3. 合并分片不是简单cat *.bin final.file校验、去重、原子写入三步缺一不可3.1 合并前必须做文件级 MD5 校验为什么不能只信chunkIndex顺序分片上传最隐蔽的坑是「网络乱序」客户端可能先发 chunk 5再发 chunk 1尤其在弱网下。如果后端只按chunkIndex数字排序合并得到的文件就是乱码。upload-demo 的解决方案是合并前先检查所有分片是否齐全再按chunkIndex排序最后用hashlib.md5()对合并后的完整文件做二次校验。# merge.py 核心逻辑 def merge_chunks(file_md5, original_filename): chunk_dir os.path.join(app.config[UPLOAD_FOLDER], chunks, file_md5) chunk_files sorted([f for f in os.listdir(chunk_dir) if f.isdigit()]) # 步骤1检查分片数量是否匹配 if len(chunk_files) ! total_chunks: raise Exception(fMissing chunks: expected {total_chunks}, got {len(chunk_files)}) # 步骤2按 chunkIndex 数字排序字符串排序会错10 2 chunk_files.sort(keylambda x: int(x)) # 步骤3流式合并避免内存爆炸 final_path os.path.join(app.config[UPLOAD_FOLDER], secure_filename(original_filename)) with open(final_path, wb) as final_file: for chunk_name in chunk_files: chunk_path os.path.join(chunk_dir, chunk_name) with open(chunk_path, rb) as f: while True: data f.read(8192) # 每次读 8KB平衡 IO 和内存 if not data: break final_file.write(data) # 步骤4MD5 校验关键 with open(final_path, rb) as f: actual_md5 hashlib.md5(f.read()).hexdigest() if actual_md5 ! file_md5: os.remove(final_path) raise Exception(fMD5 mismatch: expected {file_md5}, got {actual_md5}) return final_path注意f.read()在步骤4里会把整个文件读进内存所以生产环境必须用hashlib.md5()的增量更新模式见 3.3 节。这里只是示意逻辑。3.2 如何避免合并过程中的「竞态条件」用文件锁 临时目录双保险多个用户同时上传同名文件比如都叫report.pdf或同一用户多次上传会导致merge_chunks()被并发调用可能产生文件覆盖。upload-demo 用两层防护临时目录隔离每个文件的分片存放在UPLOAD_FOLDER/chunks/{file_md5}/下file_md5是全局唯一 ID合并锁文件在merge_chunks()开头创建.merging锁文件结束时删除。lock_file os.path.join(chunk_dir, .merging) if os.path.exists(lock_file): time.sleep(0.1) # 简单退避生产环境建议用 threading.Lock 或 redis lock continue open(lock_file, w).close() try: # 执行合并逻辑... finally: if os.path.exists(lock_file): os.remove(lock_file)3.3 生产级 MD5 校验用增量哈希避免内存溢出上面代码里f.read()会把整个文件加载进内存1GB 文件直接吃掉 1GB RAM。正确做法是边读边哈希def calculate_file_md5(filepath): hash_md5 hashlib.md5() with open(filepath, rb) as f: for chunk in iter(lambda: f.read(4096), b): # 每次读 4KB hash_md5.update(chunk) return hash_md5.hexdigest() # 合并后立即校验 final_md5 calculate_file_md5(final_path) if final_md5 ! file_md5: os.remove(final_path) raise Exception(File integrity check failed)这个iter(lambda: f.read(4096), b)是 Python 标准技巧它会不断调用f.read(4096)直到返回空 bytesb为止完美避开一次性读取大文件。4. 部署 Flask 分片上传服务gunicorn nginx 配置的三个生死线4.1 nginx 配置必须突破的两个默认限制Flask 本地测试没问题一上 nginx 就 413 Request Entity Too Large 或 504 Gateway Timeout。这是因为 nginx 默认限制了client_max_body_size控制单个请求体最大尺寸分片请求虽小但合并后的大文件上传可能走另一路proxy_read_timeout控制 nginx 等待 upstreamFlask响应的时间。# /etc/nginx/sites-available/upload-demo server { listen 80; server_name upload.example.com; # 生死线1必须设为 0无限制或大于你最大单文件尺寸 client_max_body_size 0; location / { proxy_pass http://127.0.0.1:8000; # 生死线2分片上传合并可能耗时较长尤其机械硬盘 proxy_read_timeout 3600; # 1小时足够传 10GB # 生死线3转发原始 Host 和 Connection 头避免 Flask 获取错误 URL proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }提示client_max_body_size 0表示禁用限制但需确保后端有业务层校验如if file_size 10*1024*1024*1024: abort()否则可能被恶意上传打爆磁盘。4.2 gunicorn 启动参数为什么--timeout必须大于proxy_read_timeoutgunicorn 的--timeout是 worker 进程处理单个请求的最大秒数。如果 nginx 的proxy_read_timeout设为 3600而 gunicorn 的--timeout是默认 30 秒那么上传进行到 31 秒时gunicorn 就会杀掉 worker返回 502 Bad Gateway。# 正确启动命令timeout 必须 ≥ nginx proxy_read_timeout gunicorn -w 4 -b 127.0.0.1:8000 --timeout 3600 --keep-alive 5 app:app-w 44 个 worker应对并发分片请求--keep-alive 5保持 HTTP 连接 5 秒减少 TCP 握手开销分片上传会发几十个请求。4.3 Flask 配置文件里必须显式关闭的两个危险开关app.config里有两个默认开启的选项在分片上传场景下必须关掉# config.py class Config: # 危险开关1Werkzeug 的文件缓存会把小分片暂存内存大分片反而触发磁盘缓存行为不可控 MAX_CONTENT_LENGTH None # 关闭全局请求体限制由 nginx 控制 # 危险开关2Flask 的 JSON 自动解析分片请求是 multipart/form-data不是 JSON JSONIFY_PRETTYPRINT_REGULAR False JSON_SORT_KEYS False # 安全加固上传目录绝对路径禁止相对路径 UPLOAD_FOLDER /var/www/upload-demo/uploadsMAX_CONTENT_LENGTH None是关键——它让 Flask 不再拦截request.stream把控制权完全交给 nginx。很多翻车案例都是因为开发者在 Flask 层设了MAX_CONTENT_LENGTH 100 * 1024 * 1024结果 nginx 放行了Flask 却在request.stream.read()时抛RequestEntityTooLarge。5. 避坑五个血泪换来的分片上传排错清单现象→原因→解决5.1 现象前端显示「上传完成」但服务器uploads/目录下没有文件原因WebUploader 的onUploadFinished回调触发过早未等后端合并完成就结束。upload-demo 的/merge接口是异步的但前端没监听合并结果。解决在 WebUploader 的onUploadFinished里加轮询或 WebSocket 监听合并状态。简易方案是加个 2 秒延时再提示uploader.on(uploadFinished, function() { setTimeout(() { alert(上传完成文件正在后台合并请稍候...); }, 2000); });5.2 现象同一文件上传两次uploads/下出现两个不同名字的文件如report.pdf和report (1).pdf原因前端没传originalFilename或 Flask 用secure_filename()处理时中文文件名被截断或转义导致os.path.join()生成重复路径。解决强制前端传原始文件名并在 Flask 中用urllib.parse.unquote()解码# 前端 formData: { md5: , chunkIndex: , totalChunks: , filename: encodeURIComponent(file.name) // 编码中文 } # 后端 original_filename urllib.parse.unquote(request.form.get(filename, )) safe_name secure_filename(original_filename) # 再安全化5.3 现象合并后文件能打开但播放器报「moov atom not found」MP4 视频无法播放原因MP4 文件的元数据moov box通常在文件开头但分片上传时被切散合并后 moov 可能落在文件中部。FFmpeg 无法识别。解决合并后用ffmpeg -i input.mp4 -c copy -movflags faststart output.mp4重写 moov 到开头。upload-demo 可加个 post-merge hookimport subprocess if final_path.endswith(.mp4): subprocess.run([ ffmpeg, -i, final_path, -c, copy, -movflags, faststart, final_path .fixed ]) os.replace(final_path .fixed, final_path)5.4 现象Chrome 上传正常Safari 报Failed to load resource: The network connection was lost原因Safari 对XMLHttpRequest的分片上传有更严格的 CORS 和 header 限制尤其X-Requested-With头缺失时。解决在 Flask 中显式允许所有 headers并关闭X-Requested-With校验app.after_request def after_request(response): response.headers.add(Access-Control-Allow-Origin, *) response.headers.add(Access-Control-Allow-Headers, Content-Type,Authorization,X-Requested-With) response.headers.add(Access-Control-Allow-Methods, GET,PUT,POST,DELETE,OPTIONS) return response5.5 现象用curl模拟分片上传成功但 WebUploader 总卡在 chunk 0原因WebUploader 默认发送Content-Type: multipart/form-data而curl -F发送的是multipart/form-data; boundaryxxxFlask 的request.form能解析后者但某些 Nginx 配置会 strip boundary 导致解析失败。解决在 nginx 配置里加underscores_in_headers on;并确保proxy_pass不修改 headerunderscores_in_headers on; # 允许 header 里有下划线WebUploader 用 location /upload { proxy_pass http://127.0.0.1:8000/upload; proxy_pass_request_headers on; # 必须开启 }6. 进阶技巧用 Redis 做分片状态中心实现跨服务器合并与实时进度推送6.1 为什么需要 Redis单机 Flask 部署够用但集群下分片可能落到不同 worker当用gunicorn -w 4或 Kubernetes 多 Pod 时分片请求可能被负载均衡到不同 Flask 实例。A 实例收到 chunk 0B 实例收到 chunk 1C 实例收到 chunk 2……没有共享存储合并就会失败。upload-demo 原生不支持但加 Redis 只需 20 行代码。# redis_client.py import redis r redis.Redis(hostlocalhost, port6379, db0) # 存储分片状态key fupload:{file_md5}:chunks, value set of chunk indices received def record_chunk_received(file_md5, chunk_index): r.sadd(fupload:{file_md5}:chunks, str(chunk_index)) def get_received_chunks(file_md5): return {int(x) for x in r.smembers(fupload:{file_md5}:chunks)} def is_merge_ready(file_md5, total_chunks): return len(get_received_chunks(file_md5)) total_chunks6.2 合并触发器用 Redis Pub/Sub 实现「收到最后一片立刻合并」不再轮询数据库而是用 Redis 的发布订阅模型# merge_trigger.py def listen_for_merge(): pubsub r.pubsub() pubsub.subscribe(upload_complete) for message in pubsub.listen(): if message[type] message: payload json.loads(message[data]) file_md5 payload[file_md5] original_filename payload[filename] merge_chunks(file_md5, original_filename) # 在 upload_chunk() 末尾加 if is_merge_ready(file_md5, total_chunks): r.publish(upload_complete, json.dumps({ file_md5: file_md5, filename: original_filename }))6.3 实时进度推送用 Server-Sent EventsSSE替代轮询WebUploader 的进度条只显示当前 chunk用户不知道「合并还要多久」。用 SSE 推送后端真实进度# app.py 新增路由 app.route(/progress/file_md5) def progress_stream(file_md5): def event_stream(): while True: # 从 Redis 读取当前合并进度0-100 progress r.get(fupload:{file_md5}:progress) or b0 yield fdata: {progress.decode()}\n\n time.sleep(1) return Response(event_stream(), mimetypetext/event-stream)前端用EventSource监听const eventSource new EventSource(/progress/${fileMd5}); eventSource.onmessage function(e) { const progress parseInt(e.data); $(#merge-progress).text(合并中${progress}%); };从那以后我每次上线新上传功能都强制走一遍「断网重传 → 切换浏览器 → 上传超大文件 → 查看 nginx error.log」四步验证。upload-demo 的价值不在代码多精巧而在它把分片上传里所有能踩的坑都提前用注释和结构标好了。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表