news 2026/10/27 17:02:58

Flask+WebUploader大文件分片上传、断点续传与秒传实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flask+WebUploader大文件分片上传、断点续传与秒传实践

简介:这份演示项目源码围绕 Flask 与 WebUploader 实现大文件分片上传及流式下载,面向希望理解前后端分片协作、并发上传与低内存文件传输的 Python Web 开发者,也可作为课程设计或内部工具原型参考。压缩包共 26 个文件,约 427KB,包含 5 个 CSS、5 个 JS、4 个 map、2 个 HTML 模板、2 个 Python 脚本,以及 requirements、Markdown 说明和字体图标等资源,分别承担页面渲染、分片上传交互、样式布局与服务端处理。项目支持多用户同时上传且互不干扰,接收全部分片后合并为完整文件,并提供流式下载以降低内存占用;前端借助 Bootstrap 渲染上传进度条,便于观察分片状态。已有 653 人学习,读者可从中获取分片上传流程、合并逻辑、编码问题处理及后续按序回传优化的完整思路。

1. 大文件上传为什么必须分片:一个 2GB 素材卡在 99% 的现场

某开发者把一段 2GB 的素材拖进上传框,进度条走到 99% 停住,刷新重来,第三次干脆网关超时——这个现场我见过太多次。upload-demo 是一份用 Flask 做服务端、WebUploader 做浏览器端的分段上传大文件实现:把整块请求拆成固定大小的分片,逐片落盘,最后在服务端合并。它解决的不是“能不能传”,而是“断一次要不要从头再来”。适合要在内网后台、管理页里塞大文件上传能力的人,也适合拿 BootstrapHTML 页面直接接上就跑的新手。前端要的是可控并发与重试,后端要的是稳定落盘与合并,两边都得先把“分片”这件事说清楚。

2. Flask 接收端:分片接口、落盘与合并的三段式

服务端是整个方案的承重墙。很多人第一次写分片上传,会把“接收分片”和“合并文件”塞进同一个路由,结果一个请求挂十秒,前端超时重试,分片写了两遍。合理的做法是按职责切成三条路由:一条告诉前端“哪些片已经有了”,一条收单片,一条负责合并。Flask 本身很轻,处理这种 IO 密集的活正合适,但它的默认配置和 Werkzeug 的解析行为有几个地方必须提前改掉,否则会在压测阶段集中爆出来。

2.1 三条路由的约定与参数格式

三条路由的职责边界要提前定死,前端 WebUploader 的配置项才能对着填。我一般按下面这张表来约定,字段名短、语义直白,前端在uploadBeforeSend里一次性注入就行。

路由方法关键参数返回
/upload/checkPOSTfileId、filename、chunkTotal已上传分片索引数组、是否已存在完整文件
/upload/chunkPOSTfileId、chunkIndex、chunkTotal、chunk(文件体)当前分片索引、累计收到数量
/upload/mergePOSTfileId、filename、chunkTotal合并后路径、服务端 sha256

这里有个容易忽略的点:fileId不要用文件名。同名文件在不同用户手里太常见,用文件名当目录名会让两份上传互相踩。常见做法是前端用WebUploader.File.id加时间戳拼一个短 ID,或者干脆取文件大小、修改时间、随机数做哈希。chunkTotal每次请求都带,是为了让服务端能独立判断“片齐没齐”,不必依赖前一次请求的状态。

# app.py —— 三条路由的骨架 import os from flask import Flask, request, jsonify app = Flask(__name__) BASE_DIR = os.path.abspath("upload_tmp") CHUNK_DIR = os.path.join(BASE_DIR, "chunks") FILE_DIR = os.path.join(BASE_DIR, "files") os.makedirs(CHUNK_DIR, exist_ok=True) os.makedirs(FILE_DIR, exist_ok=True) @app.route("/upload/chunk", methods=["POST"]) def upload_chunk(): file_id = request.form["fileId"] # 前端生成的上传会话 ID chunk_index = int(request.form["chunkIndex"]) total = int(request.form["chunkTotal"]) chunk = request.files["chunk"] # 字段名要和 WebUploader 的 fileVal 对齐 target_dir = os.path.join(CHUNK_DIR, file_id) os.makedirs(target_dir, exist_ok=True) # 零填充命名,保证字符串排序等于数字排序 chunk.save(os.path.join(target_dir, f"{chunk_index:06d}.part")) return jsonify({"ok": True, "index": chunk_index, "total": total})

这段代码的重点不在save,而在目录结构和命名。fileId一级目录把不同上传会话隔离开,{chunk_index:06d}.part的六位零填充是给后面合并用的——文件系统返回的listdir顺序是字典序,10.part会排在2.part前面,零填充之后000010一定在000002后面。这个细节不写,合并出来的文件就是错位的,而且错得很隐蔽:文件大小对得上,打开就报格式损坏。

2.2 分片落盘:并发写、幂等与磁盘水位

WebUploader 默认可以开多个线程并发上传分片,Flask 开发服务器默认单线程,但上了 gunicorn 或 uWSGI 就是多 worker。多个分片同时save到同一目录本身没问题,因为文件名不同;真正要防的是同一个分片被重复提交——网络抖动、前端重试、用户刷新页面都会触发。

重复写同一个.part文件,最坏情况是两次写入交错,得到一个半新半旧的分片。解决办法很土但有效:先写临时名再原子重命名。

import tempfile @app.route("/upload/chunk", methods=["POST"]) def upload_chunk_safe(): file_id = request.form["fileId"] chunk_index = int(request.form["chunkIndex"]) chunk = request.files["chunk"] target_dir = os.path.join(CHUNK_DIR, file_id) os.makedirs(target_dir, exist_ok=True) final_path = os.path.join(target_dir, f"{chunk_index:06d}.part") # 先落到同目录临时文件,再 os.replace 原子替换 fd, tmp_path = tempfile.mkstemp(dir=target_dir, suffix=".tmp") os.close(fd) chunk.save(tmp_path) os.replace(tmp_path, final_path) return jsonify({"ok": True, "index": chunk_index})

os.replace在同一文件系统内是原子操作,读到的要么是旧分片、要么是新分片,不会出现半个。tempfile.mkstemp放在target_dir里而不是系统/tmp,是为了保证os.replace不跨设备——跨设备会退化成复制加删除,原子性就没了,这个坑我在容器环境里踩过一次,排查了半天。

磁盘水位也得看一眼。分片目录会随着上传堆积,一个 2GB 文件按 5MB 切就是 400 个分片文件,再加上合并时生成的完整文件,峰值占用接近两倍。上线前用df -h确认临时目录所在分区有足够余量,别等传到 80% 才报No space left on device。

2.3 合并:流式拼接、哈希校验与目录回收

合并路由是唯一可能长时间占用请求的地方。一个 2GB 文件,即使磁盘顺序写,也要几秒到十几秒。这里最常见的翻车写法是b"".join(open(p, "rb").read() for p in parts),2GB 直接进内存,容器内存限制一卡就 OOM。

正确姿势是固定缓冲区流式拷贝,边写边算哈希:

import hashlib import shutil @app.route("/upload/merge", methods=["POST"]) def merge(): data = request.get_json() file_id = data["fileId"] filename = os.path.basename(data["filename"]) # 防目录穿越 total = int(data["chunkTotal"]) src_dir = os.path.join(CHUNK_DIR, file_id) if not os.path.isdir(src_dir): return jsonify({"ok": False, "msg": "分片目录不存在"}), 400 parts = sorted(os.listdir(src_dir)) if len(parts) != total: return jsonify({"ok": False, "msg": f"分片缺失 {len(parts)}/{total}"}), 400 out_path = os.path.join(FILE_DIR, filename) sha = hashlib.sha256() with open(out_path, "wb") as out: for name in parts: if not name.endswith(".part"): continue with open(os.path.join(src_dir, name), "rb") as fp: while True: buf = fp.read(1024 * 1024) # 1MB 缓冲区 if not buf: break sha.update(buf) out.write(buf) shutil.rmtree(src_dir, ignore_errors=True) # 合并成功再清分片 return jsonify({"ok": True, "path": out_path, "sha256": sha.hexdigest()})

几个参数值得说清楚。缓冲区取 1MB 是个折中,太小系统调用次数多,太大对内存不友好,2GB 文件下 1MB 缓冲区大约两千次读写,耗时可以接受。os.path.basename那行不能省,filename来自前端,带../就能写到任意路径。shutil.rmtree放在写完之后,顺序不能反——先删分片再合并,中途失败就彻底没得救了。

合并完成后把sha256返回给前端,前端可以再算一次本地哈希做比对。这一步在“秒传”场景里是刚需,下一章会展开。

2.4 Flask 侧三个必须先改的配置

第一是请求体大小。Flask 2.3 之前用MAX_CONTENT_LENGTH,单位字节,设成20 * 1024 * 1024表示单个请求最多 20MB,分片 5MB 的话留了四倍余量。超过限制 Werkzeug 直接返回 413,前端 WebUploader 只会看到一个uploadError,不会告诉你原因。

app.config["MAX_CONTENT_LENGTH"] = 20 * 1024 * 1024 # 单请求上限 app.config["JSON_AS_ASCII"] = False # 中文错误信息不乱码

第二是超时。合并路由耗时可能超过默认的 30 秒,gunicorn 的--timeout要放大到 120 秒以上,否则 worker 被杀,前端收到 502 但文件其实已经合并完了,用户重传一次就多一份垃圾。

第三是下载端。合并后的文件要能被流式下载,send_file的conditional=True会启用 Range 请求支持,配合前端的 stream-downloader 就能做断点续传下载,和上传侧的分片逻辑对称起来:

from flask import send_file @app.route("/download/<path:filename>") def download(filename): path = os.path.join(FILE_DIR, os.path.basename(filename)) return send_file(path, as_attachment=True, conditional=True)

conditional=True是这行的关键,没有它浏览器只能整文件下载,2GB 文件断一次就得重来,等于上传侧做了断点、下载侧又退回原点。

3. WebUploader 前端:chunkSize、threads 与重试参数怎么定

前端这层看着只是配置项,实际决定了服务端要承受什么样的压力曲线。chunkSize调小,请求数变多,握手和鉴权开销翻倍;调大,单请求失败重传成本高,弱网下体验反而差。WebUploader 的分片能力是内置的,但它默认不会把分片索引按你想要的字段名传,也不会自动做断点续传,事件链得自己接。

3.1 初始化参数:chunked、chunkSize、threads 的组合

先看一份能直接跑的初始化配置:

var uploader = WebUploader.create({ swf: '/static/webuploader/Uploader.swf', server: '/upload/chunk', pick: '#picker', fileVal: 'chunk', // 必须和 Flask 的 request.files['chunk'] 对齐 chunked: true, chunkSize: 5 * 1024 * 1024, // 5MB 一片 chunkRetry: 3, // 单片失败重试次数 threads: 3, // 同时上传的分片数 duplicate: true, // 允许重复选择同一文件 resize: false, formData: { fileId: '', chunkTotal: 0 } });

chunkSize取 5MB 是我在多数内网环境下的默认值。理由是:小于 1MB,400 个分片变成 2000 个,请求头开销和 Flask 的解析开销都会明显上来;大于 10MB,弱网下单请求超时概率上升,重传一次就是 10MB 的浪费。5MB 在两者之间,400 个分片对 2GB 文件来说,目录里文件数不至于多到listdir变慢。

threads: 3要结合服务端 worker 数看。gunicorn 开 4 个 worker,前端开 8 个线程,多出来的请求就在队列里排着,反而拉高了单请求耗时。我的经验值是前端线程数不超过后端 worker 数的 75%。

duplicate: true不是可有可无的。默认false时,用户第二次选同一个文件会被静默忽略,看起来像“点了没反应”,这个反馈缺失在后台系统里被投诉过很多次。

3.2 事件链:uploadBeforeSend 注入分片索引

WebUploader 内部其实会带chunks、chunk、id这些字段,但字段名和服务端约定对不上,最稳的做法是在uploadBeforeSend里显式补齐:

uploader.on('uploadBeforeSend', function (block, data, headers) { var file = block.file; data.fileId = file.id; // 本次上传会话 ID data.chunkIndex = block.chunk; // 当前分片索引,从 0 开始 data.chunkTotal = Math.ceil(file.size / uploader.option('chunkSize')); data.filename = file.name; });

block.chunk是当前分片的序号,block.file是文件对象,file.id在页面生命周期内唯一。chunkTotal每次都重算而不是缓存,是因为chunkSize可能在运行中被改(比如根据网络状况动态调整),缓存值会和服务端收到的实际分片数对不上。

分片全部传完后 WebUploader 会触发uploadSuccess,注意这个事件是每个分片成功都会触发一次,不是整个文件完成才触发。判断整个文件完成要靠计数器:

var doneMap = {}; uploader.on('uploadSuccess', function (file, response) { if (!doneMap[file.id]) doneMap[file.id] = 0; doneMap[file.id]++; var total = Math.ceil(file.size / uploader.option('chunkSize')); if (doneMap[file.id] === total) { // 所有分片到齐,发起合并 $.post('/upload/merge', { fileId: file.id, filename: file.name, chunkTotal: total }, function (res) { if (res.ok) console.log('合并完成', res.sha256); }); } });

这段代码里doneMap用file.id做 key,页面刷新就丢了,所以断点续传必须依赖服务端的/upload/check,不能靠前端内存状态。

3.3 Bootstrap 页面骨架与进度条绑定

upload-demo 的页面用 Bootstrap 排版,结构很简单:一个选择按钮、一个文件列表、一个总进度条。关键是进度条的数据来源——WebUploader 的uploadProgress事件给的是单个分片的进度,要汇总成文件级进度得自己算:

<div class="card"> <div class="card-body"> <div id="picker" class="btn btn-primary">选择文件</div> <div class="progress mt-3" style="height: 22px;"> <div id="bar" class="progress-bar" role="progressbar" style="width: 0%;">0%</div> </div> <ul id="filelist" class="list-group mt-3"></ul> </div> </div>
var progressMap = {}; uploader.on('uploadProgress', function (file, percentage) { progressMap[file.id] = percentage; // 单分片进度 var loaded = 0, total = 0; uploader.getFiles().forEach(function (f) { var size = f.size || 0; total += size; loaded += size * (progressMap[f.id] || 0); }); var pct = total ? Math.floor(loaded / total * 100) : 0; $('#bar').css('width', pct + '%').text(pct + '%'); });

getFiles()返回当前队列里的所有文件,多文件同时上传时进度条是加权平均,不会出现“一个传完 100%、另一个刚开始 0%”的跳变。Bootstrap 的.progress-bar只负责样式,宽度靠内联 style 控制,别去改它的 CSS 类名。

3.4 暂停、继续与断点续传的接线

WebUploader 自带uploader.stop()和uploader.upload(),但这两个是全局的。要做“单文件暂停”,得用uploader.stop(true)加文件 ID 过滤,或者干脆在uploadBeforeSend里拦截已暂停的文件。

更实用的场景是页面刷新后的续传。流程是:用户重新选同一个文件 → 前端调/upload/check→ 服务端返回已收到的分片索引 → 前端跳过这些分片。WebUploader 没有“跳过指定分片”的公开 API,常见做法是给分片请求加一个拦截:

var uploadedSet = {}; // 由 /upload/check 填充 uploader.on('uploadBeforeSend', function (block, data) { if (uploadedSet[block.file.id] && uploadedSet[block.file.id].indexOf(block.chunk) > -1) { block.status = 'skipped'; // 标记后由 uploadAccept 拦截 return false; } });

return false会让 WebUploader 放弃这个分片的发送,但不会把它计入成功计数,所以合并触发逻辑要单独处理“跳过的分片视为已完成”。这块逻辑容易写漏,测试时务必覆盖“传到一半刷新页面再续传”的路径。

4. 秒传与断点续传:文件指纹、分片索引和临时目录治理

分片上传做完,下一个自然需求就是“同一个文件别传第二遍”。秒传听着高级,本质是一次哈希比对:前端算出文件指纹,服务端查一下有没有,有就直接返回路径。问题在于 2GB 文件的哈希怎么算才不把浏览器卡死,以及算出来的指纹怎么和分片索引配合。

4.1 整文件 MD5 在 2GB 上的代价

直接SparkMD5.hash(file)是行不通的。FileReader.readAsArrayBuffer一次性读 2GB,浏览器内存直接爆,标签页崩溃。正确做法是切片增量计算:

function calcFileMd5(file, onProgress) { return new Promise(function (resolve, reject) { var sliceSize = 4 * 1024 * 1024; // 每次读 4MB var chunks = Math.ceil(file.size / sliceSize); var current = 0; var spark = new SparkMD5.ArrayBuffer(); var reader = new FileReader(); function loadNext() { var start = current * sliceSize; var end = Math.min(start + sliceSize, file.size); reader.readAsArrayBuffer(file.slice(start, end)); } reader.onload = function (e) { spark.append(e.target.result); current++; onProgress && onProgress(Math.floor(current / chunks * 100)); if (current < chunks) { loadNext(); } else { resolve(spark.end()); } }; reader.onerror = reject; loadNext(); }); }

4MB 的切片大小是经验值,太小回调次数多,太大会让单次读取的停顿感明显。这个函数即使切片读,2GB 也要跑十几秒,期间主线程被FileReader回调占着,页面会卡。缓解办法是放到 Web Worker 里跑,或者先做“抽样哈希”——取文件头 1MB、尾 1MB、中间若干块算一个弱指纹,先做粗筛,命中后再算全量。

提示:秒传的判定标准要业务方确认。仅凭文件名和大小判重会误伤,两个不同文件同名同大小的情况真实存在,哈希是底线。

4.2 /upload/check 的返回结构与续传逻辑

校验接口返回什么,直接决定前端续传代码好不好写。我倾向于返回三个字段,语义清晰:

@app.route("/upload/check", methods=["POST"]) def check(): data = request.get_json() file_id = data["fileId"] filename = os.path.basename(data["filename"]) total = int(data["chunkTotal"]) src_dir = os.path.join(CHUNK_DIR, file_id) uploaded = [] if os.path.isdir(src_dir): for name in os.listdir(src_dir): if name.endswith(".part"): uploaded.append(int(name.split(".")[0])) finished = os.path.exists(os.path.join(FILE_DIR, filename)) return jsonify({ "uploaded": sorted(uploaded), "finished": finished, "missing": [i for i in range(total) if i not in uploaded] })

uploaded是已落盘的分片,missing是还缺的,finished表示服务端已有完整文件——这三个字段组合起来,前端就能判断是“秒传直接完成”“部分续传”还是“全新上传”。返回missing而不是让前端自己算差集,是为了避免chunkTotal不一致时的边界错误:分片大小改过之后,旧会话的索引对不上新会话,前端自己算容易越界。

前端拿到missing后,把不在列表里的分片标记为跳过,只传缺失的。合并触发条件改成“已上传数 + 跳过数 == chunkTotal”,不再依赖前端计数器。

4.3 临时目录的生命周期管理

分片目录不会自己消失。用户传了一半关页面、合并失败、秒传命中后旧分片还在,这些都会留下垃圾。一个 2GB 文件传一半就是 1GB 的残留,几十个用户就能把磁盘吃满。

治理思路是两条:合并成功后立即删(上一章的shutil.rmtree),以及定时清理超时目录。定时任务用find就够,不必上调度框架:

#!/usr/bin/env bash # 清理 24 小时未变动的分片目录,先打印再删,便于观察 find /data/upload_tmp/chunks -mindepth 1 -maxdepth 1 -type d -mmin +1440 \ -print -exec rm -rf {} \;

-mmin +1440是 24 小时,按目录的修改时间算。这里有个取舍:如果用户传一个超大文件超过 24 小时,目录会一直被写,mtime会刷新,不会被误删;但如果用户中途暂停超过 24 小时,分片就被清了,续传时得从头来。这个阈值要根据业务实际的上传时长分布来调,后台系统里设 48 小时更稳妥。

完整文件目录也要有对应策略。/download路由提供的是最终产物,不能随便删,但可以按“最后访问时间”做冷热分层,或者交给对象存储的生命周期规则,这就超出 upload-demo 的范围了。

5. 避坑与排查:分片错位、413 与合并丢字节的五个现场

这套东西跑通不难,难的是跑稳。下面五条都是我在实际部署里遇到的,按“现象 → 原因 → 解决”记下来,遇到了可以对着排查。

5.1 现象:合并成功但文件打不开,大小对得上

原因几乎都是分片顺序错。os.listdir返回的是目录项顺序,不保证字典序,更不保证数字序。如果文件名没做零填充,10.part会排在2.part前面,合并出来就是乱的。

解决办法是双重保险:落盘时用{index:06d}.part零填充,合并时先sorted()再按后缀过滤,最后校验总片数和期望值是否一致。已经出了问题的文件没法救,只能删了重传。

5.2 现象:5MB 分片请求返回 413,小文件却正常

原因是有两层限制同时存在。Flask 的MAX_CONTENT_LENGTH是一层,前面的 Nginx 还有一层client_max_body_size,默认 1MB。只改 Flask 不改 Nginx,请求在网关就被拒了,Flask 根本收不到。

解决是两边都放大,并且留余量。分片 5MB,Nginx 设 20MB,Flask 设 20MB,multipart 的边界和表单字段也有开销,贴着 5MB 设会偶发超限。

client_max_body_size 20m; proxy_read_timeout 300s; proxy_request_buffering off; # 大文件直传,别让 Nginx 先缓冲整个请求

proxy_request_buffering off这行容易被忽略。开着的时候 Nginx 会把整个请求体先收完再转给后端,分片上传的并发优势被抵消一半,内存压力也上去了。

5.3 现象:选完文件浏览器卡死几秒,进度条不动

原因是主线程在算 MD5。FileReader的回调虽然异步,但SparkMD5.append是同步计算,2GB 数据分四百次追加,每次都在主线程跑,页面自然卡。

解决办法优先是挪到 Web Worker,把File对象postMessage过去(File是可转移对象),算完把哈希传回来。退一步的做法是先算抽样指纹做秒传粗筛,只有粗筛命中才跑全量哈希。

5.4 现象:合并时容器内存飙到限制,进程被 kill

原因是把分片一次性读进内存拼接。b"".join(...)或者read()不带参数,都会把整个文件加载进来。2GB 文件在 512MB 内存限制的容器里必死。

解决就是上一章那段流式写法:固定 1MB 缓冲区,while循环读写,哈希增量更新。改完之后合并 2GB 文件的内存占用稳定在几 MB,CPU 也平稳。

5.5 现象:两个用户同时上传,分片互相覆盖

原因是fileId撞了。常见于用文件名或文件大小当 ID 的实现,两个人传同名文件就进同一个目录,后写的分片覆盖先写的,合并出来的文件两边都不对。

解决办法是fileId必须包含足够随机的成分,前端用file.id加时间戳,服务端再校验一次格式(比如只允许[A-Za-z0-9_-]{8,64}),防止路径穿越。目录创建用os.makedirs(exist_ok=True),两个请求同时创建不会报错。

6. 压测与验证:用 curl 灌分片、核对哈希、跑一遍断点场景

界面点通了不算数,我一般会用 curl 绕过前端,直接对服务端灌分片,这样能把前端的问题和后端的问题分开。第一步是把一个测试文件切成 5MB 的片:

# 生成 30MB 测试文件并切片,-d 用数字后缀,-a 6 补齐六位 dd if=/dev/urandom of=demo.bin bs=1M count=30 split -b 5m -d -a 6 demo.bin part_ ls part_* | wc -l # 应该是 6

split出来的文件名是part_000000这种格式,和 Flask 侧的零填充命名规则一致,但 curl 提交时要用分片索引而不是文件名。循环里10#$i是为了把带前导零的字符串按十进制解析,否则 bash 会当成八进制,08直接报错:

for i in $(seq -w 0 5); do curl -s -X POST http://127.0.0.1:5000/upload/chunk \ -F "fileId=test-20240101-001" \ -F "chunkIndex=$((10#$i))" \ -F "chunkTotal=6" \ -F "chunk=@part_$i" \ -o /dev/null -w "chunk $i -> %{http_code}\n" done

跑完看每个分片的 HTTP 状态码,全是 200 才算落盘正常。接着调合并接口,把返回的sha256和本地算的比一下:

curl -s -X POST http://127.0.0.1:5000/upload/merge \ -H "Content-Type: application/json" \ -d '{"fileId":"test-20240101-001","filename":"demo.bin","chunkTotal":6}' sha256sum demo.bin

两个哈希一致,说明分片落盘、合并、哈希计算整条链路没问题。不一致就往回查:先看分片目录里文件数和大小对不对,再看合并顺序,最后看缓冲区读写有没有漏字节。

第二步是模拟断点。故意只传前三个分片,然后调/upload/check,看missing是不是[3,4,5]:

curl -s -X POST http://127.0.0.1:5000/upload/check \ -H "Content-Type: application/json" \ -d '{"fileId":"test-20240101-001","filename":"demo.bin","chunkTotal":6}'

返回对得上,再补传后三个分片、合并、比哈希。这一步能抓出大部分索引相关的边界错误,比在浏览器里点来点去可靠得多。

第三步是并发验证。开三个终端同时跑上面的循环,但用不同的fileId,观察服务端 CPU 和磁盘 IO,以及合并耗时是否线性增长。如果并发一上来合并就超时,回去看 gunicorn 的--timeout和 worker 数。

最后一步是清理验证。合并成功后立刻ls upload_tmp/chunks,确认对应目录已删;再手动造一个超时的空目录,跑一次find ... -mmin +1440的清理脚本,确认能删干净。这步容易被跳过,等到磁盘告警才想起来就晚了。

这套流程我现在每次改完上传相关代码都会强制走一遍,尤其是哈希比对那步——有一次本地测试图省事只看了文件大小,结果线上合并出来差了几百字节,前端下载后打不开,回滚排查了两个小时。从那以后我每次改完分片逻辑,都把 curl 灌分片加sha256sum比对当成固定动作,宁可多花三分钟,也不赌运气。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 17:01:47

Django工作流引擎实战:五张表构建工单流转系统

简介&#xff1a;这是一套基于Django框架实现的工作流引擎与工单系统完整项目源码&#xff0c;面向Python Web开发学习者、软件工程专业学生及需要业务流程管理方案的开发者&#xff0c;尤其适合作为毕业设计选题。项目以Python为主要语言&#xff0c;围绕工作流定义、任务分配…

作者头像 李华
网站建设 2026/10/27 17:01:32

YOLO包装盒缺陷检测:数据格式与训练参数避坑指南

简介&#xff1a;YOLO算法快递包裹包装盒缺陷检测数据集包含1200余张标注图像&#xff0c;覆盖完好箱体、破损箱体、开口包裹和完整包裹四类目标&#xff0c;适合入门至进阶学习者练习目标检测模型的训练与评估。数据集已划分train、val、test目录&#xff0c;并附data.yaml配置…

作者头像 李华
网站建设 2026/10/27 17:00:41

LLM模型评测全指南:基准选择、数据污染与可复现流程

如果你已经花了几个月时间折腾数据集清洗、LoRA 微调、分布式训练&#xff0c;结果模型一上线&#xff0c;连你自己都说不清它到底比 base 强在哪&#xff0c;那我建议你停下来&#xff0c;认真读一读这篇。LLM 训练做到后期&#xff0c;真正拉开差距的往往不是训练脚本写得多花…

作者头像 李华
网站建设 2026/10/27 17:00:21

STM32温湿度采集报警系统实战:DHT11时序、代码与避坑指南

简介&#xff1a;这份资源是面向嵌入式初学者与STM32开发者的温湿度采集报警系统完整工程包&#xff0c;围绕环境监测与超限报警场景展开&#xff0c;可用于农业、仓储、智能家居等方向的课程设计或练手项目。压缩包共204个文件&#xff0c;约3.29MB&#xff0c;以52个c源文件与…

作者头像 李华
网站建设 2026/10/27 16:59:34

CAD矢量图在富文本编辑器的精准粘贴技术方案

1. 问题背景与行业痛点在芯片制造企业的日常研发流程中&#xff0c;CAD图纸的跨平台流转是个高频刚需。我接触过某半导体设备厂商的案例&#xff1a;他们的工艺工程师需要将AutoCAD设计的掩膜版图纸粘贴到企业知识管理系统的富文本编辑器&#xff08;TinyMCE&#xff09;中&…

作者头像 李华