大文件下载内存溢出:JavaScript+PHP分块下载方案咨询
大文件中转下载的内存问题解决方案
问题背景
搭建了一套文件中转下载系统:用户输入目标文件URL(比如Freepik图片),PHP系统通过带API密钥的请求从指定端点获取文件,再中转给用户。当前用readfile()的方案对小文件有效,但大文件会触发内存限制错误,希望通过JS+PHP优化解决。
当前PHP中转代码:
<?php $filename = $_GET['filename']; $url = $_GET['url']; header('Content-Type: application/octet-stream'); header("Content-Transfer-Encoding: Binary"); header("Content-disposition: attachment; filename=\"$filename\""); readfile(urldecode($url));
当前前端JS下载代码:
async function download(post_id) { var url = jQuery('input[name="url"]').val(); var filename = jQuery('input[name="filename"]').val(); const fd = new FormData(); fd.append('url', url); fd.append('filename', filename); return fetch(myAjax.ajaxurl, { method: 'POST', body: fd, }) .then((result) => { if (!result.ok) { throw Error(result.statusText); } const header = result.headers.get('Content-Disposition'); const parts = header.split(';'); filename = parts[1].split('=')[1].replaceAll('"', ''); return result.blob(); }) .then((blob) => { if (blob != null) { var url = window.URL.createObjectURL(blob); var a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); } }) .catch((err) => { console.log(err); }); }
核心解决方案:PHP流式输出(无需分块)
readfile()处理远程文件时,PHP可能会先把整个文件读入内存导致溢出。改用cURL流式获取并实时输出,完全规避内存占用问题:
改进后的PHP中转代码
<?php // 校验参数,拦截恶意请求 if (empty($_POST['url']) || empty($_POST['filename'])) { http_response_code(400); exit('缺少必要参数'); } $targetUrl = urldecode($_POST['url']); $filename = $_POST['filename']; // 设置响应头,告知浏览器处理下载 header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Transfer-Encoding: chunked'); header('Connection: keep-alive'); // 初始化cURL请求 $ch = curl_init($targetUrl); // 关键设置:不缓存到内存,实时输出内容 curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) { echo $data; ob_flush(); // 立即输出缓冲区 flush(); return strlen($data); }); // 若需要携带API密钥,添加请求头 // curl_setopt($ch, CURLOPT_HTTPHEADER, ['Authorization: Bearer YOUR_API_KEY']); // 执行请求并输出 curl_exec($ch); curl_close($ch); exit;
前端代码适配
无需大幅修改,改用流式读取响应,避免大文件占用浏览器内存:
async function download(post_id) { const url = jQuery('input[name="url"]').val(); const filename = jQuery('input[name="filename"]').val(); if (!url || !filename) { console.error('缺少URL或文件名'); return; } const fd = new FormData(); fd.append('url', url); fd.append('filename', filename); try { const response = await fetch(myAjax.ajaxurl, { method: 'POST', body: fd, }); if (!response.ok) { throw new Error(`请求失败:${response.statusText}`); } // 解析后端返回的文件名 const contentDisposition = response.headers.get('Content-Disposition'); let downloadFilename = filename; if (contentDisposition) { const match = contentDisposition.match(/filename="?([^"]+)"?/); downloadFilename = match ? match[1] : filename; } // 流式处理响应,避免内存溢出 const reader = response.body.getReader(); const stream = new ReadableStream({ async start(controller) { while (true) { const { done, value } = await reader.read(); if (done) break; controller.enqueue(value); } controller.close(); } }); const blob = await new Response(stream).blob(); const blobUrl = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = blobUrl; a.download = downloadFilename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(blobUrl); }, 100); } catch (err) { console.error('下载出错:', err); } }
可选:分块下载方案(适配超大型文件)
如果文件体积达到数GB级别,可实现分块下载,前端逐块请求,后端返回对应片段,最后合并:
后端PHP分块处理代码
<?php if (empty($_POST['url']) || empty($_POST['filename']) || !isset($_POST['start']) || !isset($_POST['end'])) { http_response_code(400); exit('缺少必要参数'); } $targetUrl = urldecode($_POST['url']); $filename = $_POST['filename']; $start = (int)$_POST['start']; $end = (int)$_POST['end']; // 获取目标文件总大小 $ch = curl_init($targetUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_HEADER, true); curl_setopt($ch, CURLOPT_NOBODY, true); curl_exec($ch); $fileSize = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD); curl_close($ch); // 修正结束位置,避免超出文件大小 if ($end > $fileSize - 1) { $end = $fileSize - 1; } // 设置分块响应头 header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize); header('Content-Length: ' . ($end - $start + 1)); header('Transfer-Encoding: chunked'); // 请求并输出指定文件块 $ch = curl_init($targetUrl); curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Range: bytes=' . $start . '-' . $end]); curl_setopt($ch, CURLOPT_WRITEFUNCTION, function($ch, $data) { echo $data; ob_flush(); flush(); return strlen($data); }); // 添加API密钥头 // curl_setopt($ch, CURLOPT_HTTPHEADER, [..., 'Authorization: Bearer YOUR_API_KEY']); curl_exec($ch); curl_close($ch); exit;
前端分块下载合并代码
async function downloadLargeFile(post_id) { const url = jQuery('input[name="url"]').val(); const filename = jQuery('input[name="filename"]').val(); if (!url || !filename) { console.error('缺少URL或文件名'); return; } // 先获取文件总大小(需后端配合返回) let fileSize; try { const fd = new FormData(); fd.append('url', url); fd.append('getSize', true); const sizeResponse = await fetch(myAjax.ajaxurl, { method: 'POST', body: fd, }); const data = await sizeResponse.json(); fileSize = data.size; } catch (e) { console.error('获取文件大小失败:', e); return; } const chunkSize = 1024 * 1024 * 5; // 5MB每块 const chunks = []; let start = 0; while (start < fileSize) { const end = Math.min(start + chunkSize - 1, fileSize - 1); try { const fd = new FormData(); fd.append('url', url); fd.append('filename', filename); fd.append('start', start); fd.append('end', end); const response = await fetch(myAjax.ajaxurl, { method: 'POST', body: fd, }); if (!response.ok) { throw new Error(`块${start}-${end}下载失败`); } const blob = await response.blob(); chunks.push(blob); start = end + 1; console.log(`已下载${start}/${fileSize}字节`); } catch (e) { console.error('下载块出错:', e); return; } } // 合并所有块并触发下载 const combinedBlob = new Blob(chunks); const blobUrl = window.URL.createObjectURL(combinedBlob); const a = document.createElement('a'); a.href = blobUrl; a.download = filename; document.body.appendChild(a); a.click(); setTimeout(() => { document.body.removeChild(a); window.URL.revokeObjectURL(blobUrl); }, 100); }
内容的提问来源于stack exchange,提问作者Aks Jacoves
相关产品推荐
相关产品推荐

