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

大文件下载内存溢出: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 15:57:10