PHP 大文件上传、断点续传完整实现思路
PHP 大文件上传、断点续传完整实现思路一、为什么需要断点续传传统上传的痛点假设你要上传一个 2GB 的视频文件问题表现后果网络中断上传到80%时WiFi断了重新开始前功尽弃超时限制PHPmax_execution_time默认30秒大文件必然超时内存溢出upload_max_filesize限制超过2M直接被拒用户体验差进度条卡死、无法暂停用户流失断点续传的核心价值失败恢复从中断处继续而非从头开始分片加速多片并行上传充分利用带宽暂停/继续用户可随时控制上传节奏校验完整性确保文件传输无损坏二、整体架构设计客户端 服务端 ┌─────────────┐ ┌──────────────────┐ │ 文件分片 │ ──分片1──▶ │ 临时目录存储分片 │ │ 计算MD5 │ ──分片2──▶ │ 记录上传状态 │ │ 记录进度 │ ──分片3──▶ │ 合并验证完整性 │ │ 断点恢复 │ ◀──响应── │ 返回已上传分片 │ └─────────────┘ └──────────────────┘三、前端实现HTML JavaScript3.1 HTML 基础结构!DOCTYPE html html head title断点续传上传/title style .progress-bar { width: 300px; height: 24px; background: #eee; border-radius: 12px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #4CAF50, #45a049); transition: width 0.3s; } .chunk-status { margin-top: 10px; font-size: 13px; color: #666; } /style /head body input typefile idfileInput / button onclickstartUpload()开始上传/button button onclickpauseUpload()暂停/button div classprogress-bar div classprogress-fill idprogressFill stylewidth: 0%/div /div div classchunk-status idstatus/div script srcuploader.js/script /body /html3.2 核心上传逻辑uploader.jsclass ChunkUploader { constructor(file, chunkSize 1024 * 1024) { this.file file; this.chunkSize chunkSize; // 每片大小默认1MB this.chunks Math.ceil(file.size / chunkSize); this.uploadedChunks new Set(); // 已上传的分片索引 this.isPaused false; this.fileHash ; // 文件唯一标识 this.abortController null; // 用于取消请求 } // 第一步计算文件哈希唯一标识 async calculateHash() { return new Promise((resolve, reject) { const worker new Worker(hash-worker.js); worker.postMessage({ file: this.file }); worker.onmessage (e) { if (e.data.type progress) { // 可选显示哈希计算进度 } else if (e.data.type complete) { this.fileHash e.data.hash; resolve(this.fileHash); } }; worker.onerror reject; }); } // 第二步检查已上传状态 async checkUploadStatus() { const response await fetch(/check-upload, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ file_hash: this.fileHash, file_name: this.file.name, total_size: this.file.size }) }); const data await response.json(); if (data.status completed) { return { needUpload: false, url: data.file_url }; } // 返回已上传的分片列表 this.uploadedChunks new Set(data.uploaded_chunks || []); return { needUpload: true, uploadedChunks: this.uploadedChunks }; } // 第三步上传单个分片 async uploadChunk(index) { const start index * this.chunkSize; const end Math.min(start this.chunkSize, this.file.size); const blob this.file.slice(start, end); const formData new FormData(); formData.append(chunk, blob); formData.append(index, index); formData.append(total_chunks, this.chunks); formData.append(file_hash, this.fileHash); formData.append(file_name, this.file.name); try { const response await fetch(/upload-chunk, { method: POST, body: formData, signal: this.abortController?.signal }); if (!response.ok) throw new Error(分片${index}上传失败); this.uploadedChunks.add(index); this.updateProgress(); return true; } catch (error) { if (error.name AbortError) { console.log(上传被暂停); return false; } throw error; } } // 第四步控制上传流程 async startUpload() { this.isPaused false; this.abortController new AbortController(); // 1. 计算文件哈希 document.getElementById(status).textContent 正在计算文件指纹...; await this.calculateHash(); // 2. 检查已有进度 document.getElementById(status).textContent 检查已上传部分...; const status await this.checkUploadStatus(); if (!status.needUpload) { alert(文件已存在: status.url); return; } // 3. 逐片上传可改为并行上传 document.getElementById(status).textContent 开始上传...; for (let i 0; i this.chunks; i) { if (this.isPaused) break; if (this.uploadedChunks.has(i)) continue; // 跳过已上传 await this.uploadChunk(i); } // 4. 通知服务端合并 if (!this.isPaused) { await this.mergeFile(); } } // 第五步通知合并 async mergeFile() { const response await fetch(/merge-chunks, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ file_hash: this.fileHash, file_name: this.file.name, total_chunks: this.chunks }) }); const result await response.json(); if (result.success) { alert(上传完成文件地址 result.url); } } // 暂停上传 pauseUpload() { this.isPaused true; this.abortController?.abort(); } // 更新进度条 updateProgress() { const percent (this.uploadedChunks.size / this.chunks) * 100; document.getElementById(progressFill).style.width percent %; document.getElementById(status).textContent 已上传 ${this.uploadedChunks.size}/${this.chunks} 分片; } } // 启动上传 async function startUpload() { const fileInput document.getElementById(fileInput); if (!fileInput.files[0]) { alert(请选择文件); return; } window.uploader new ChunkUploader(fileInput.files[0], 2 * 1024 * 1024); // 2MB分片 await window.uploader.startUpload(); } function pauseUpload() { window.uploader?.pauseUpload(); }3.3 Web Worker 计算文件哈希hash-worker.js:self.importScripts(https://cdnjs.cloudflare.com/ajax/libs/spark-md5/3.0.0/spark-md5.min.js); self.onmessage function(e) { const file e.data.file; const chunkSize 2 * 1024 * 1024; // 2MB读取块 const chunks Math.ceil(file.size / chunkSize); let currentChunk 0; const spark new SparkMD5.ArrayBuffer(); const reader new FileReader(); reader.onload function(event) { spark.append(event.target.result); currentChunk; self.postMessage({ type: progress, progress: (currentChunk / chunks) * 100 }); if (currentChunk chunks) { loadNext(); } else { self.postMessage({ type: complete, hash: spark.end() }); } }; reader.onerror function() { self.postMessage({ type: error, message: 文件读取失败 }); }; function loadNext() { const start currentChunk * chunkSize; const end Math.min(start chunkSize, file.size); reader.readAsArrayBuffer(file.slice(start, end)); } loadNext(); };四、后端实现PHP4.1 目录结构project/ ├── uploads/ # 最终文件存储目录 │ └── chunks/ # 临时分片目录 │ └── {file_hash}/ # 每个文件一个子目录 │ ├── 0.part │ ├── 1.part │ └── ... ├── upload_handler.php # 上传处理入口 ├── check_status.php # 检查上传状态 └── merge_chunks.php # 合并分片4.2 配置文件?php // config.php define(UPLOAD_DIR, __DIR__ . /uploads/); define(CHUNK_DIR, UPLOAD_DIR . chunks/); define(MAX_FILE_SIZE, 10 * 1024 * 1024 * 1024); // 10GB define(ALLOWED_EXTENSIONS, [mp4, avi, zip, pdf, docx]); define(CHUNK_EXPIRE_HOURS, 48); // 未完成的分片保留时间 // 确保目录存在 if (!is_dir(UPLOAD_DIR)) mkdir(UPLOAD_DIR, 0755, true); if (!is_dir(CHUNK_DIR)) mkdir(CHUNK_DIR, 0755, true);4.3 检查上传状态?php // check_status.php require_once config.php; header(Content-Type: application/json); if ($_SERVER[REQUEST_METHOD] ! POST) { http_response_code(405); exit(json_encode([error Method not allowed])); } $data json_decode(file_get_contents(php://input), true); $fileHash $data[file_hash] ?? ; $fileName $data[file_name] ?? ; if (!$fileHash || !$fileName) { http_response_code(400); exit(json_encode([error 参数缺失])); } $chunkDir CHUNK_DIR . $fileHash . /; $finalPath UPLOAD_DIR . $fileName; // 情况1文件已经完整上传 if (file_exists($finalPath)) { echo json_encode([ status completed, file_url /uploads/ . $fileName, uploaded_chunks [] ]); exit; } // 情况2有部分分片 $uploadedChunks []; if (is_dir($chunkDir)) { $files scandir($chunkDir); foreach ($files as $file) { if (preg_match(/^(\d)\.part$/, $file, $matches)) { $uploadedChunks[] (int)$matches[1]; } } sort($uploadedChunks); } echo json_encode([ status in_progress, uploaded_chunks $uploadedChunks, file_url null ]);4.4 接收分片上传?php // upload_chunk.php require_once config.php; header(Content-Type: application/json); if ($_SERVER[REQUEST_METHOD] ! POST) { http_response_code(405); exit(json_encode([error Method not allowed])); } // 验证参数 $requiredFields [index, total_chunks, file_hash, file_name]; foreach ($requiredFields as $field) { if (!isset($_POST[$field])) { http_response_code(400); exit(json_encode([error 缺少参数: {$field}])); } } $chunkIndex (int)$_POST[index]; $totalChunks (int)$_POST[total_chunks]; $fileHash $_POST[file_hash]; $fileName basename($_POST[file_name]); // 防止路径穿越 // 验证文件扩展名 $ext strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); if (!in_array($ext, ALLOWED_EXTENSIONS)) { http_response_code(403); exit(json_encode([error 不支持的文件类型])); } // 验证文件大小 if (!isset($_FILES[chunk]) || $_FILES[chunk][error] ! UPLOAD_ERR_OK) { http_response_code(400); exit(json_encode([error 分片上传失败])); } // 创建分片目录 $chunkDir CHUNK_DIR . $fileHash . /; if (!is_dir($chunkDir)) { mkdir($chunkDir, 0755, true); } // 保存分片文件 $chunkPath $chunkDir . $chunkIndex . .part; if (!move_uploaded_file($_FILES[chunk][tmp_name], $chunkPath)) { http_response_code(500); exit(json_encode([error 保存分片失败])); } // 可选校验分片完整性 $expectedSize $chunkIndex $totalChunks - 1 ? CHUNK_SIZE : filesize($_FILES[chunk][tmp_name]); // 最后一片可能较小 echo json_encode([ success true, index $chunkIndex, received_size filesize($chunkPath) ]);4.5 合并分片?php // merge_chunks.php require_once config.php; header(Content-Type: application/json); if ($_SERVER[REQUEST_METHOD] ! POST) { http_response_code(405); exit(json_encode([error Method not allowed])); } $data json_decode(file_get_contents(php://input), true); $fileHash $data[file_hash] ?? ; $fileName basename($data[file_name] ?? ); $totalChunks (int)($data[total_chunks] ?? 0); if (!$fileHash || !$fileName || !$totalChunks) { http_response_code(400); exit(json_encode([error 参数缺失])); } $chunkDir CHUNK_DIR . $fileHash . /; $finalPath UPLOAD_DIR . $fileName; // 检查是否所有分片都存在 for ($i 0; $i $totalChunks; $i) { $chunkPath $chunkDir . $i . .part; if (!file_exists($chunkPath)) { http_response_code(400); exit(json_encode([ error 缺少分片 {$i}, missing_index $i ])); } } // 合并文件流式写入避免内存爆炸 $finalHandle fopen($finalPath, wb); if (!$finalHandle) { http_response_code(500); exit(json_encode([error 无法创建目标文件])); } for ($i 0; $i $totalChunks; $i) { $chunkPath $chunkDir . $i . .part; $chunkHandle fopen($chunkPath, rb); // 流式复制每次读取8KB while (!feof($chunkHandle)) { fwrite($finalHandle, fread($chunkHandle, 8192)); } fclose($chunkHandle); unlink($chunkPath); // 删除分片 } fclose($finalHandle); // 清理空目录 rmdir($chunkDir); // 验证最终文件 $actualSize filesize($finalPath); $expectedSize array_sum(array_map(filesize, glob($chunkDir . *.part))); echo json_encode([ success true, file_url /uploads/ . $fileName, size $actualSize ]);4.6 统一入口路由?php // index.php - 简单路由 $action $_GET[action] ?? ; switch ($action) { case check: require check_status.php; break; case upload: require upload_chunk.php; break; case merge: require merge_chunks.php; break; default: http_response_code(404); echo json_encode([error 未知操作]); }五、高级优化5.1 并行上传浏览器端// 在 startUpload 方法中替换循环为并行 async uploadAllChunks() { const CONCURRENCY 5; // 并发数 const tasks []; for (let i 0; i this.chunks; i) { if (this.uploadedChunks.has(i)) continue; tasks.push(() this.uploadChunk(i)); } // 使用并发控制 const results []; const executing new Set(); for (const task of tasks) { if (this.isPaused) break; const promise task().then(result { executing.delete(promise); return result; }); executing.add(promise); results.push(promise); if (executing.size CONCURRENCY) { await Promise.race(executing); } } await Promise.all(results); }5.2 服务端限速与防滥用// 在 upload_chunk.php 中添加 // IP 频率限制 $ip $_SERVER[REMOTE_ADDR]; $rateKey upload_rate:{$ip}; $currentCount $redis-incr($rateKey); $redis-expire($rateKey, 60); if ($currentCount 600) { // 每分钟最多600次请求 http_response_code(429); exit(json_encode([error 请求过于频繁])); } // 磁盘空间检查 $freeSpace disk_free_space(UPLOAD_DIR); if ($freeSpace 500 * 1024 * 1024) { // 剩余不足500MB http_response_code(507); exit(json_encode([error 服务器存储空间不足])); }5.3 定时清理过期分片?php // cleanup.php - 由cron定期执行 require_once config.php; $expireTime time() - (CHUNK_EXPIRE_HOURS * 3600); $dirs glob(CHUNK_DIR . *, GLOB_ONLYDIR); foreach ($dirs as $dir) { $mtime filemtime($dir); if ($mtime $expireTime) { // 递归删除目录 $files glob($dir . /*); foreach ($files as $file) { unlink($file); } rmdir($dir); } } echo Cleanup completed at . date(Y-m-d H:i:s);六、安全性考虑风险防护措施路径遍历攻击使用basename()过滤文件名任意文件覆盖基于文件哈希隔离存储超大文件耗尽磁盘检查disk_free_space()恶意分片填充限制单个IP的上传速率文件类型欺骗服务端验证文件头魔数并发写入冲突使用flock()加锁合并七、总结这套断点续传方案的核心优势可靠性任意中断都能恢复不丢失数据高效性并行上传充分利用带宽可扩展支持TB级文件上传低成本纯PHP实现无需额外组件适用场景视频平台的大文件上传云盘系统的文件同步企业内部的资料归档日志收集系统的批量导入通过分片、哈希校验和状态追踪三个核心机制我们让PHP也能优雅地处理GB级别的文件上传。关键在于把大问题拆解成小问题再逐个解决。

相关新闻

SpringBoot项目从零搭建:环境配置、项目结构与数据库整合实战

SpringBoot项目从零搭建:环境配置、项目结构与数据库整合实战

1. 项目概述:为什么SpringBoot是Java开发者的“瑞士军刀”如果你是一名Java开发者,或者正准备踏入这个领域,那么“SpringBoot”这个名字你一定不陌生。它几乎成了现代Java企业级应用开发的代名词。但很多新手,甚至一些有经验的开发…

2026/7/30 11:21:33 阅读更多 →
终极指南:用SillyTavern打造有灵魂的AI角色,告别机械对话

终极指南:用SillyTavern打造有灵魂的AI角色,告别机械对话

终极指南:用SillyTavern打造有灵魂的AI角色,告别机械对话 【免费下载链接】SillyTavern LLM Frontend for Power Users. 项目地址: https://gitcode.com/GitHub_Trending/si/SillyTavern 你是否厌倦了那些只会机械回答的AI角色?想要创…

2026/7/30 11:20:33 阅读更多 →
Python JSON文件写入换行问题解析与JSON Lines格式实战

Python JSON文件写入换行问题解析与JSON Lines格式实战

1. 从一次数据导出异常说起:为什么JSON文件会“粘”在一起?前几天,我帮一个做数据分析的朋友处理一批用户行为日志。他的需求很简单:用Python脚本清洗数据,然后把清洗后的每条记录,以JSON格式追加写入到一个…

2026/7/30 11:20:33 阅读更多 →

最新新闻

告别Steam限制:这款免费下载器让你轻松获取1000+游戏模组

告别Steam限制:这款免费下载器让你轻松获取1000+游戏模组

告别Steam限制:这款免费下载器让你轻松获取1000游戏模组 【免费下载链接】WorkshopDL WorkshopDL - The Best Steam Workshop Downloader 项目地址: https://gitcode.com/gh_mirrors/wo/WorkshopDL 你是否曾在其他平台购买了心仪的游戏,却发现最精…

2026/7/30 11:28:35 阅读更多 →
3步掌握AI视频生成:MoneyPrinterTurbo全流程实战指南

3步掌握AI视频生成:MoneyPrinterTurbo全流程实战指南

3步掌握AI视频生成:MoneyPrinterTurbo全流程实战指南 【免费下载链接】MoneyPrinterTurbo 利用 AI 大模型和自动化工作流,根据主题或关键词一键生成高清短视频。Generate HD short videos from a topic or keyword with an automated AI workflow. 项目…

2026/7/30 11:28:35 阅读更多 →
Python循环控制语句break、continue与pass详解

Python循环控制语句break、continue与pass详解

1. Python循环控制语句的本质区别在Python编程中,break、continue和pass这三个看似简单的关键字,实际构成了循环控制的三叉戟。我见过太多初学者在for和while循环中滥用这些语句,导致代码逻辑混乱不堪。让我们从底层机制开始,彻底…

2026/7/30 11:28:35 阅读更多 →
5步搞定Windows Edge管理:自动化卸载与重装终极方案

5步搞定Windows Edge管理:自动化卸载与重装终极方案

5步搞定Windows Edge管理:自动化卸载与重装终极方案 【免费下载链接】EdgeRemover A PowerShell script that correctly uninstalls or reinstalls Microsoft Edge on Windows 10 & 11. 项目地址: https://gitcode.com/gh_mirrors/ed/EdgeRemover EdgeRe…

2026/7/30 11:28:35 阅读更多 →
XUnity Auto Translator:Unity游戏实时翻译框架的原理、部署与实战

XUnity Auto Translator:Unity游戏实时翻译框架的原理、部署与实战

1. 项目概述:为什么我们需要XUnity Auto Translator? 如果你是一个喜欢玩各种独立游戏或者小众Unity游戏的玩家,或者你是一个游戏汉化组的成员,那么“游戏内置文本无法翻译”这个问题,你一定深有体会。游戏开发者可能只…

2026/7/30 11:28:35 阅读更多 →
货架行业采购决策转向AI调研 GEO优化打造SKU级精准获客通路

货架行业采购决策转向AI调研 GEO优化打造SKU级精准获客通路

当仓储物流园的采购负责人需要测算重型横梁货架的承重方案,当连锁便利店的拓展人员要对比本地商超货架的定制周期,越来越多的从业者不再先翻阅行业名录或逐个联系经销商,而是打开 AI 工具发起提问。生成式 AI 正在从信息入口层面重构货架行业…

2026/7/30 11:27:35 阅读更多 →

日新闻

Windows驱动存储终极清理工具:DriverStoreExplorer完全指南

Windows驱动存储终极清理工具:DriverStoreExplorer完全指南

Windows驱动存储终极清理工具:DriverStoreExplorer完全指南 【免费下载链接】DriverStoreExplorer Driver Store Explorer 项目地址: https://gitcode.com/gh_mirrors/dr/DriverStoreExplorer 您是否曾因Windows系统盘空间不足而烦恼?是否遇到过设…

2026/7/30 0:00:13 阅读更多 →
如何3步掌握Video Download Helper:网页视频下载的完整实战指南

如何3步掌握Video Download Helper:网页视频下载的完整实战指南

如何3步掌握Video Download Helper:网页视频下载的完整实战指南 【免费下载链接】VideoDownloadHelper Chrome Extension to Help Download Video for Some Video Sites. 项目地址: https://gitcode.com/gh_mirrors/vi/VideoDownloadHelper 你是否曾经在浏览…

2026/7/30 0:00:13 阅读更多 →
“双减”后首个AI备课压力测试报告:覆盖32所中小学的176节AI辅助课,暴露4大隐性增负节点

“双减”后首个AI备课压力测试报告:覆盖32所中小学的176节AI辅助课,暴露4大隐性增负节点

更多请点击: https://intelliparadigm.com 第一章:AI 教师备课辅助 AI 教师备课辅助系统正逐步成为教育数字化转型的核心支撑工具,它并非替代教师,而是通过语义理解、知识图谱与多模态生成能力,将教师从重复性劳动中解…

2026/7/30 0:00:13 阅读更多 →

周新闻

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 道路桥梁裂缝检测数据集 道路桥梁病害识别检测数据集

深度学习道路桥梁裂缝检测系统 数据集6000张 完整源码已标注数据集训练好的模型环境配置教程程序运行说明文档,可以直接使用!系统支持图片、视频、摄像头等多种方式检测裂缝,功能强大实用。 1数据集6000张 8各类别

2026/7/29 22:18:20 阅读更多 →
深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

深度学习YOLO模型如何训练 PUBG 绝地求生目标检测数据集

pubg数据集 精选原图1.42万数据 1.49万标签 无任何重复、算法增强或冗余图像! pubg绝地求生目标检测数据集 1分类:e_body,14905个标签,txt格式 共计14244张图,99%为640*640尺寸图像 适合yolo目标检测、AI训练关键词&am…

2026/7/29 14:34:28 阅读更多 →
Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex英雄目标检测数据集 深度学习框架YOLO如何训练APEX数据集

Apex检测数据集数据集详情检测类别: allies enemy tag图片总量:7247张训练集:5139张验证集:1425张测试集:683张标注状态:全部已标注,即拿即用数据格式:支持YOLO格式及其他格式&#…

2026/7/29 15:00:03 阅读更多 →

月新闻