突破php.ini限制:1GB+大文件XHR分片上传实现方案问询
大文件上传优化方案(针对自托管文件传输服务)
问题背景
原代码是一个简易拖拽上传服务,仅支持100MB以下文件,核心痛点:
- 受限于PHP配置(
upload_max_filesize、post_max_size)无法上传大文件 - 使用
file_get_contents一次性读取文件到内存,超过服务器内存时直接崩溃 - 需要支持1GB+文件,同时解决内存占用问题,实现可靠的大文件上传
一、基础配置调整(非分片临时方案)
若仅需临时突破单次上传大小限制,可修改PHP配置参数(php.ini或项目根目录.htaccess):
upload_max_filesize = 1Gpost_max_size = 1.2G(需大于upload_max_filesize)memory_limit = 256M(无需匹配文件大小,后续代码优化可进一步降低)max_execution_time = 300(延长执行时间,避免上传超时)max_input_time = 300
但该方案仍存在内存占用过高、网络中断导致上传失败的问题,分片上传是更优的长期解决方案。
二、内存友好的文件处理(非分片)
原代码中file_get_contents($_FILES['data']['tmp_name'])会将整个文件加载到内存,替换为直接移动临时文件或逐块写入,彻底避免内存溢出:
修改后端PHP代码:
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST') { $localfname = $_POST['fname']; // 生成随机前缀避免文件名冲突 $fname = ''; for ($i = 0; $i < 4; $i++) { $fname .= '0123456789abcdefghijklmnopqrstuvwxyz'[rand(0, 35)]; } $trimmedfname = preg_replace('/\s+/', '', basename($localfname)); $targetPath = 'files/' . $fname . '_' . $trimmedfname; // 直接移动临时文件,无需加载到内存 if (move_uploaded_file($_FILES['data']['tmp_name'], $targetPath)) { $link = $targetPath; echo 'link: <div id="link"><a href="' . $link . '">' . $link . '</a></div>'; } else { echo '上传失败'; } die(); } ?>
三、分片上传实现(推荐,支持1GB+文件)
分片上传将大文件分割为多个固定大小的小片段(如10MB/块),逐个上传后在服务器合并,优势:
- 突破PHP单次上传大小限制
- 内存占用极低
- 支持断点续传(可跳过已上传的块)
前端实现(修改JS部分)
<div id="container" style="width: 200px; height: 200px; background-color: #eee; padding: 1em;">drag and drop your file here!</div> <script> var $ = document.querySelector.bind(document); const CHUNK_SIZE = 10 * 1024 * 1024; // 10MB每块 async function uploadFile(file) { const fileId = Math.random().toString(36).substring(2, 10); // 唯一标识当前文件 const totalChunks = Math.ceil(file.size / CHUNK_SIZE); let uploadedChunks = 0; $("#container").innerHTML = `准备上传... 总块数: ${totalChunks}`; // 逐块上传 for (let i = 0; i < totalChunks; i++) { const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, file.size); const chunk = file.slice(start, end); const formData = new FormData(); formData.append('fname', file.name); formData.append('fileId', fileId); formData.append('chunkIndex', i); formData.append('totalChunks', totalChunks); formData.append('chunk', chunk); await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('POST', ''); xhr.onload = () => { if (xhr.status === 200) { uploadedChunks++; $("#container").innerHTML = `上传中... ${uploadedChunks}/${totalChunks} (${Math.round(uploadedChunks/totalChunks*100)}%)`; resolve(); } else { reject(new Error('块上传失败')); } }; xhr.onerror = reject; xhr.send(formData); }); } // 所有块上传完成,请求服务器合并文件 const mergeForm = new FormData(); mergeForm.append('action', 'merge'); mergeForm.append('fileId', fileId); mergeForm.append('fname', file.name); const mergeXhr = new XMLHttpRequest(); mergeXhr.open('POST', ''); mergeXhr.onload = () => { $("#container").innerHTML = mergeXhr.responseText; }; mergeXhr.send(mergeForm); } var readfiles = files => { uploadFile(files[0]); } document.body.ondragover = () => { $("#container").innerHTML = 'drop your file here...'; return false; }; document.body.ondrop = (e) => { e.preventDefault(); readfiles(e.dataTransfer.files); }; </script>
后端实现(修改PHP部分)
<?php // 确保存储目录存在 if (!is_dir('files')) mkdir('files', 0755); if (!is_dir('temp')) mkdir('temp', 0755); if ($_SERVER['REQUEST_METHOD'] === 'POST') { // 处理分片上传 if (isset($_POST['chunkIndex'])) { $fileId = $_POST['fileId']; $chunkIndex = $_POST['chunkIndex']; $tempPath = "temp/{$fileId}_{$chunkIndex}"; // 保存分片到临时目录 move_uploaded_file($_FILES['chunk']['tmp_name'], $tempPath); echo 'ok'; die(); } // 处理文件合并 if ($_POST['action'] === 'merge') { $fileId = $_POST['fileId']; $localfname = $_POST['fname']; // 生成随机前缀 $fname = ''; for ($i = 0; $i < 4; $i++) { $fname .= '0123456789abcdefghijklmnopqrstuvwxyz'[rand(0, 35)]; } $trimmedfname = preg_replace('/\s+/', '', basename($localfname)); $targetPath = 'files/' . $fname . '_' . $trimmedfname; // 合并所有分片 $targetFile = fopen($targetPath, 'wb'); $chunkIndex = 0; while (file_exists("temp/{$fileId}_{$chunkIndex}")) { $chunk = fopen("temp/{$fileId}_{$chunkIndex}", 'rb'); stream_copy_to_stream($chunk, $targetFile); fclose($chunk); unlink("temp/{$fileId}_{$chunkIndex}"); // 删除临时分片 $chunkIndex++; } fclose($targetFile); $link = $targetPath; echo 'link: <div id="link"><a href="' . $link . '">' . $link . '</a></div>'; die(); } } ?>
分片上传关键细节
- 文件标识:用
fileId唯一标识每个文件,避免同名文件冲突 - 分片大小:建议10-50MB,太小会增加请求次数,太大容易超时
- 断点续传:可在前端记录已上传的块,上传前先请求服务器校验已存在的块,跳过重复上传
- 临时文件清理:可定时清理
temp目录中未合并的过期分片
内容的提问来源于stack exchange,提问作者Basj
相关产品推荐
相关产品推荐

