登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  php教程

PHP 表单上传大文件怎么做断点续传:分片校验、临时文件与合并

来源:17golang原创

时间:2026-08-25 01:13:05 415浏览 收藏

大文件上传最麻烦的地方不是把一个 $_FILES 写进磁盘,而是用户传到 80% 时网络断了,下一次打开页面还得从头来。更稳妥的做法是把文件拆成固定大小的分片,每片单独校验并落到任务目录,服务端只在所有分片齐全且顺序正确时合并成最终文件。

断点续传的核心不是“记住上传进度”,而是让客户端能查询已完成分片,并让服务端用文件哈希、分片编号和临时目录把一次上传任务区分开。

要点速览:
  • 用 upload_id 隔离每一次上传任务。
  • 分片写入临时目录并在改名后返回成功。
  • 合并前检查分片总数,完成后核对整文件哈希。
  • 失败任务保留状态并按规则清理临时文件。

先把上传协议定清楚

建议把一次上传抽象成三个接口:创建任务、上传分片、查询与合并。客户端第一次提交文件名、文件总大小、总分片数和整文件哈希,服务端返回不可猜测的 upload_id。后续请求只携带这个 ID、分片编号、分片总数和当前二进制内容。

分片编号从 0 开始,服务端文件名统一使用 000000.part 这样的格式,不直接相信客户端传来的原始文件名。原始文件名只作为展示字段,最终保存名应由服务端生成。

POST /upload/init
{
  "name": "backup.zip",
  "size": 524288000,
  "parts": 50,
  "sha256": "整文件哈希"
}

PHP 接收分片时先做边界校验

上传接口收到请求后,第一件事是检查任务是否存在、分片编号是否在范围内、分片大小是否超过上限。不要把用户传来的文件名直接拼进路径,也不要用“文件存在就算成功”的判断,因为上次中断可能留下半截文件。

= $total) {
    http_response_code(422);
    exit('invalid part metadata');
}

$root = __DIR__ . '/runtime/uploads/' . basename($uploadId);
if (!is_dir($root) && !mkdir($root, 0700, true)) {
    throw new RuntimeException('cannot create upload directory');
}
$target = $root . '/' . sprintf('%06d.part', $part);
$input = fopen('php://input', 'rb');
$output = fopen($target . '.tmp', 'wb');
stream_copy_to_stream($input, $output);
fclose($input);
fclose($output);
rename($target . '.tmp', $target);
echo json_encode(['ok' => true, 'part' => $part]);

示例中的临时文件和改名动作很重要:合并逻辑永远只读取已经改名完成的 .part 文件,不会碰到正在写入的临时文件。生产环境还应限制任务目录的属主和访问权限,并把上传 ID 与登录用户绑定。

查询接口要返回“已完成哪些片”

客户端重试前先调用查询接口。服务端扫描当前任务文件夹,只认符合六位数字编号的分片,并返回编号数组、总分片数和任务状态。不要只返回一个百分比,百分比无法告诉客户端缺的是哪一片。

$done = [];
foreach (glob($root . '/*.part') as $file) {
    if (preg_match('/\/(\d{6})\.part$/', $file, $m)) {
        $done[] = (int) $m[1];
    }
}
sort($done);
echo json_encode(['parts' => $done, 'status' => 'uploading']);

如果查询结果中已经有当前分片,客户端可以跳过它;如果只有临时文件,没有对应的 .part 文件,就继续上传。这样即使浏览器刷新,也不会把“正在写入”误当成成功。

合并前后分别做一次检查

合并接口不能只检查目录里有多少文件。它应该按 0 到 total - 1 逐片打开,确认每个分片存在,再使用二进制方式追加到一个新的临时目标文件。任何一片缺失都返回可重试的状态,而不是生成一个看似完整的坏文件。

$mergedTmp = $root . '/result.tmp';
$out = fopen($mergedTmp, 'wb');
for ($i = 0; $i 

合并完成后再计算最终文件的 SHA-256,并与初始化时记录的值比较。哈希不一致时,删除最终文件并把任务标记为失败,保留缺片信息供客户端重新上传。不要因为 HTTP 200 就认为文件可用。

错误码要让调用方知道下一步

上传接口可以用 422 表示元数据错误,用 404 表示任务不存在,用 409 表示分片缺失或任务状态冲突,用 507 表示磁盘空间不足。响应体再补一个稳定的业务字段,例如 part_missingchecksum_mismatchuploading,前端就能区分“重新传这一片”和“重新创建任务”。

重试也要有边界:同一分片可以重传,合并请求可以带幂等键,但不要在服务端无限重试文件操作。磁盘写满、权限错误和哈希不一致都需要留下日志,方便定位。

清理策略决定了断点续传能否长期运行

临时目录不能永久保留。初始化任务时记录 updated_at,每次成功写片或查询时更新它;定时清理脚本只删除超过保留时间、且不在合并状态的任务。清理前再次读取状态,避免和合并请求同时操作同一目录。

最终文件写入完成后,删除分片和任务元数据,只保留结果文件、整文件哈希和必要的审计信息。用户取消上传时也走同一套清理逻辑,不要把删除路径交给客户端拼接。

上线前用四种中断场景验收

先传完前 30% 后断网,刷新页面,确认查询接口能返回已完成分片;再重复上传一片,确认不会产生重复内容;随后人为删除一片再调用合并,确认返回 409 且没有残留结果;最后修改一个分片内容,确认整文件哈希校验失败。

还要检查 PHP 的 upload_max_filesizepost_max_size、反向代理请求体限制和磁盘配额。分片大小即使低于 PHP 限制,也可能被 Nginx 或网关拦截,验收不能只在本机完成。

相关问题

分片大小应该固定吗?

第一版建议固定在 5MB 到 20MB 之间,并根据移动网络和网关限制调整。固定大小更容易计算编号、查询缺片和重试,最后一片可以小于标准大小。

为什么不直接用原始文件名保存?

原始文件名可能包含路径片段、特殊字符或同名冲突。使用服务端生成的 upload_id 和最终文件名,能把展示信息与存储路径分开。

合并接口需要加锁吗?

需要保证同一任务不会同时合并。可以用数据库状态从 uploading 原子切换到 merging,并让重复请求读取已有结果;不要只依靠目录里是否存在 result.tmp。

把“能续传”落实成可验证的状态机

一套可靠的 PHP 断点续传流程,至少要让创建、分片写入、查询、合并、校验和清理这些动作都有明确状态。客户端负责记录 upload_id,服务端负责验证每片和最终结果。只要先把协议和失败边界定下来,后续替换前端组件或存储介质,也不会重新陷入“断了就从头传”的问题。

PHP 大文件断点续传中分片编号、校验和服务端临时目录的对应关系

PHP 断点续传合并前检查缺片、合并后整文件校验与失败清理流程

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>