首页
统计
友链
留言
导航
微语
关于
更多
直播
壁纸
视频
Search
1
十周年下载
65 阅读
2
go程序在后台运行
6 阅读
3
欢迎使用 Typecho
5 阅读
4
45 个常用Linux 命令,让你轻松玩转Linux!
3 阅读
5
Linux安装Docker完整教程
3 阅读
Linux
php
问道手游
主机
软件
Search
标签搜索
Linux常用命令
Linux安装Docker
php
内网穿透
mysql
问道下载
狗道下载
曼威
累计撰写
13
篇文章
累计收到
0
条评论
今日撰写
0
篇文章
首页
栏目
Linux
php
问道手游
主机
软件
页面
统计
友链
留言
导航
微语
关于
直播
壁纸
视频
搜索到
1
篇与
php
的结果
2026-08-07
PHP 大文件上传、断点续传完整实现思路
PHP 大文件上传 + 断点续传|完整实现思路传统 move_uploaded_file 一把梭只能应付小文件。真上了 GB 级视频 / 安装包 / 日志归档,必须上 分片 + 断点续传。本文给一套可落地的纯 PHP 实现。一、为什么必须做断点续传?假设上传一个 2GB 视频,传统方式的坑:痛点表现后果网络中断传到 80% WiFi 断了从头再来,前功尽弃超时限制max_execution_time 默认 30s大文件必超时内存/大小限制upload_max_filesize 默认 2M直接被拒体验差进度条卡死、不能暂停用户流失断点续传核心价值✅ 失败恢复:从中断处继续,不重头传✅ 分片并行:吃满带宽✅ 可暂停 / 继续✅ 哈希校验:保证文件完整性二、整体架构客户端 服务端 ┌─────────────────┐ ┌─────────────────────┐ │ 文件分片 │ ── 分片1 ──▶ │ 临时目录存分片 │ │ 计算 MD5 指纹 │ ── 分片2 ──▶ │ 记录上传状态 │ │ 本地记录进度 │ ── 分片3 ──▶ │ 合并 + 校验 │ │ 断点恢复 │ ◀─ 响应 ─── │ 返回已传分片列表 │ └─────────────────┘ └─────────────────────┘三条主线:分片化 · 哈希标识 · 状态追踪。三、前端实现3.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 type="file" id="fileInput" /> <button onclick="startUpload()">开始上传</button> <button onclick="pauseUpload()">暂停</button> <div class="progress-bar"> <div class="progress-fill" id="progressFill" style="width: 0%"></div> </div> <div class="chunk-status" id="status"></div> <script src="uploader.js"></script> </body> </html>3.2 核心上传类(uploader.js)class 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; } // 1. 计算文件哈希(Web Worker 中做,不卡 UI) 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 === 'complete') { this.fileHash = e.data.hash; resolve(this.fileHash); } }; worker.onerror = reject; }); } // 2. 询问服务端已传分片 async checkUploadStatus() { const res = 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 res.json(); if (data.status === 'completed') return { needUpload: false, url: data.file_url }; this.uploadedChunks = new Set(data.uploaded_chunks || []); return { needUpload: true }; } // 3. 上传单分片 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 fd = new FormData(); fd.append('chunk', blob); fd.append('index', index); fd.append('total_chunks', this.chunks); fd.append('file_hash', this.fileHash); fd.append('file_name', this.file.name); const res = await fetch('/upload-chunk', { method: 'POST', body: fd, signal: this.abortController?.signal }); if (!res.ok) throw new Error(`分片 ${index} 失败`); this.uploadedChunks.add(index); this.updateProgress(); } // 4. 主控流程 async startUpload() { this.isPaused = false; this.abortController = new AbortController(); await this.calculateHash(); const status = await this.checkUploadStatus(); if (!status.needUpload) return alert('文件已存在: ' + status.url); for (let i = 0; i < this.chunks; i++) { if (this.isPaused) break; if (this.uploadedChunks.has(i)) continue; await this.uploadChunk(i); } if (!this.isPaused) await this.mergeFile(); } async mergeFile() { const res = 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 r = await res.json(); if (r.success) alert('上传完成:' + r.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 f = document.getElementById('fileInput').files[0]; if (!f) return alert('请选择文件'); window.uploader = new ChunkUploader(f, 2 * 1024 * 1024); // 2MB 分片 await window.uploader.startUpload(); } function pauseUpload() { window.uploader?.pauseUpload(); }3.3 Web Worker 算 MD5(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; const chunks = Math.ceil(file.size / chunkSize); let current = 0; const spark = new SparkMD5.ArrayBuffer(); const reader = new FileReader(); reader.onload = function (ev) { spark.append(ev.target.result); if (++current < chunks) loadNext(); else self.postMessage({ type: 'complete', hash: spark.end() }); }; function loadNext() { const s = current * chunkSize; reader.readAsArrayBuffer(file.slice(s, Math.min(s + chunkSize, file.size))); } loadNext(); };{hint|关键点:哈希放 Worker 里算,2GB 文件也不会冻住主线程}四、后端(PHP)4.1 目录结构project/ ├── uploads/ │ ├── chunks/{file_hash}/0.part, 1.part ... │ └── 最终文件 ├── config.php ├── check_status.php ├── upload_chunk.php └── merge_chunks.php4.2 配置 config.php<?php define('UPLOAD_DIR', __DIR__ . '/uploads/'); define('CHUNK_DIR', UPLOAD_DIR . 'chunks/'); define('MAX_FILE_SIZE', 10 * 1024 * 1024 * 1024); define('ALLOWED_EXTENSIONS', ['mp4', 'avi', 'zip', 'pdf', 'docx']); define('CHUNK_EXPIRE_HOURS', 48); is_dir(UPLOAD_DIR) || mkdir(UPLOAD_DIR, 0755, true); is_dir(CHUNK_DIR) || mkdir(CHUNK_DIR, 0755, true);4.3 检查已传状态 check_status.php<?php require_once 'config.php'; header('Content-Type: application/json'); $data = json_decode(file_get_contents('php://input'), true); $hash = $data['file_hash'] ?? ''; $name = $data['file_name'] ?? ''; if (!$hash || !$name) http_response_code(400), exit(json_encode(['error' => '参数缺失'])); $final = UPLOAD_DIR . $name; if (file_exists($final)) { exit(json_encode(['status' => 'completed', 'file_url' => '/uploads/' . $name])); } $dir = CHUNK_DIR . $hash . '/'; $uploaded = []; if (is_dir($dir)) { foreach (scandir($dir) as $f) { if (preg_match('/^(\d+)\.part$/', $f, $m)) $uploaded[] = (int)$m[1]; } sort($uploaded); } echo json_encode(['status' => 'in_progress', 'uploaded_chunks' => $uploaded]);4.4 接收分片 upload_chunk.php<?php require_once 'config.php'; header('Content-Type: application/json'); foreach (['index','total_chunks','file_hash','file_name'] as $k) { if (!isset($_POST[$k])) http_response_code(400), exit(json_encode(['error' => "缺 {$k}"])); } $idx = (int)$_POST['index']; $total = (int)$_POST['total_chunks']; $hash = $_POST['file_hash']; $name = basename($_POST['file_name']); // 防路径穿越 $ext = strtolower(pathinfo($name, 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' => '分片接收失败'])); $dir = CHUNK_DIR . $hash . '/'; is_dir($dir) || mkdir($dir, 0755, true); $path = $dir . $idx . '.part'; if (!move_uploaded_file($_FILES['chunk']['tmp_name'], $path)) http_response_code(500), exit(json_encode(['error' => '保存失败'])); echo json_encode(['success' => true, 'index' => $idx, 'size' => filesize($path)]);4.5 合并分片 merge_chunks.php(流式,不爆内存)<?php require_once 'config.php'; header('Content-Type: application/json'); $d = json_decode(file_get_contents('php://input'), true); $hash = $d['file_hash'] ?? ''; $name = basename($d['file_name'] ?? ''); $total = (int)($d['total_chunks'] ?? 0); if (!$hash || !$name || !$total) http_response_code(400), exit(json_encode(['error' => '参数缺失'])); $dir = CHUNK_DIR . $hash . '/'; for ($i = 0; $i < $total; $i++) { if (!file_exists($dir . $i . '.part')) http_response_code(400), exit(json_encode(['error' => "缺分片 {$i}", 'missing_index' => $i])); } $final = fopen(UPLOAD_DIR . $name, 'wb'); for ($i = 0; $i < $total; $i++) { $part = fopen($dir . $i . '.part', 'rb'); while (!feof($part)) fwrite($final, fread($part, 8192)); fclose($part); unlink($dir . $i . '.part'); } fclose($final); rmdir($dir); echo json_encode(['success' => true, 'file_url' => '/uploads/' . $name, 'size' => filesize(UPLOAD_DIR . $name)]);4.6 统一入口 index.php<?php match ($_GET['action'] ?? '') { 'check' => require 'check_status.php', 'upload' => require 'upload_chunk.php', 'merge' => require 'merge_chunks.php', default => http_response_code(404), };五、高级优化5.1 前端并发上传(并发度 5)把 startUpload 里的串行 for 换成:async uploadAllChunks(concurrency = 5) { const queue = []; for (let i = 0; i < this.chunks; i++) if (!this.uploadedChunks.has(i)) queue.push(() => this.uploadChunk(i)); const exec = new Set(); for (const task of queue) { if (this.isPaused) break; const p = task().then(() => exec.delete(p)); exec.add(p); if (exec.size >= concurrency) await Promise.race(exec); } }5.2 服务端限速 / 防滥用(Redis 频率 + 磁盘检查)$ip = $_SERVER['REMOTE_ADDR']; $cnt = $redis->incr("upload_rate:$ip"); $redis->expire("upload_rate:$ip", 60); if ($cnt > 600) http_response_code(429), exit(json_encode(['error' => '请求过快'])); if (disk_free_space(UPLOAD_DIR) < 500 * 1024 * 1024) http_response_code(507), exit(json_encode(['error' => '磁盘不足']));5.3 定时清理孤儿分片(cron 调 cleanup.php)<?php require 'config.php'; $expire = time() - CHUNK_EXPIRE_HOURS * 3600; foreach (glob(CHUNK_DIR . '*', GLOB_ONLYDIR) as $dir) { if (filemtime($dir) < $expire) { array_map('unlink', glob($dir . '/*')); rmdir($dir); } } echo 'cleanup ok ', date('Y-m-d H:i:s');六、安全清单大文件入口是攻击面,下面几条必须做。风险防护路径遍历basename() 过滤文件名覆盖已有文件按 file_hash 隔离分片目录磁盘被打满disk_free_space() 前置检查分片灌水单 IP 频率限制(Redis)文件类型伪造服务端校验文件头魔数合并并发冲突flock() 加锁合并区七、小结{note|这套方案的特点}🧩 可靠:任意中断可恢复,不丢数据⚡ 高效:分片并行,吃满带宽📈 可扩展:TB 级也能跑🪶 零依赖:纯 PHP + 原生 JS,无额外组件适用:视频平台上传、网盘同步、企业归档、日志批量导入。核心就三件事:把大文件拆小、用哈希做身份、用状态表做记忆。附:完整项目文件清单index.html(前端页面)将第三章 3.1 的 HTML 结构保存为 index.html 即可。uploader.js(前端核心逻辑)将第三章 3.2 的 JavaScript 代码保存为 uploader.js。hash-worker.js(Web Worker 哈希计算)将第三章 3.3 的代码保存为 hash-worker.js。config.php(PHP 配置)将第四章 4.2 的代码保存为 config.php。check_status.php(检查上传状态)将第四章 4.3 的代码保存为 check_status.php。upload_chunk.php(接收分片)将第四章 4.4 的代码保存为 upload_chunk.php。merge_chunks.php(合并分片)将第四章 4.5 的代码保存为 merge_chunks.php。index.php(统一入口路由)将第四章 4.6 的代码保存为 index.php。cleanup.php(定时清理)将第五章 5.3 的代码保存为 cleanup.php。💡 部署提示:将所有 .php 文件放在同一目录,确保 uploads/ 目录可写,配置 cron 定时执行 cleanup.php 即可。
2026年08月07日
2 阅读
0 评论
0 点赞