You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

突破php.ini限制:1GB+大文件XHR分片上传实现方案问询

大文件上传优化方案(针对自托管文件传输服务)

问题背景

原代码是一个简易拖拽上传服务,仅支持100MB以下文件,核心痛点:

  • 受限于PHP配置(upload_max_filesize、post_max_size)无法上传大文件
  • 使用file_get_contents一次性读取文件到内存,超过服务器内存时直接崩溃
  • 需要支持1GB+文件,同时解决内存占用问题,实现可靠的大文件上传

一、基础配置调整(非分片临时方案)

若仅需临时突破单次上传大小限制,可修改PHP配置参数(php.ini或项目根目录.htaccess):

  • upload_max_filesize = 1G
  • post_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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 08:47:18