【问题标题】:Downloading large files reliably in PHP在 PHP 中可靠地下载大文件
【发布时间】:2010-10-10 11:25:19
【问题描述】:

我在服务器上有一个 php 脚本来发送文件给recipents:他们得到一个唯一的链接,然后他们可以下载大文件。有时传输出现问题,文件已损坏或永远不会完成。我想知道是否有更好的方法来发送大文件

代码:

$f = fopen(DOWNLOAD_DIR.$database[$_REQUEST['fid']]['filePath'], 'r');
while(!feof($f)){
    print fgets($f, 1024);
}
fclose($f);

见过这样的功能

http_send_file
http_send_data

但我不确定它们是否会起作用。

解决这个问题的最佳方法是什么?

问候
欧翼

【问题讨论】:

标签: php download


【解决方案1】:

对于下载文件,我能想到的最简单的方法是将文件放在一个临时位置,并给他们一个唯一的 URL,他们可以通过常规 HTTP 下载。

作为生成这些链接的一部分,您还可以删除超过 X 小时的文件。

【讨论】:

  • 我不喜欢这个答案,因为它需要额外使用 cron 等来删除旧文件,这增加了另一层复杂性和系统的另一个故障点,但我不会投反对票,因为它是一个有效的答案。
  • @Unkwntech - 不需要 cron,正如在生成新文件时提到的那样,您也可以丢弃旧文件。许多网站在另一个调用中执行类似 cron 的任务。
  • @Andrew Grant,你是对的,它可以在没有 CRON 的情况下完成,但我仍然觉得它增加了额外的失败点,而我的回答我认为它不会增加一个额外的故障点。
  • 但是您从中得到的是将实际 HTTP 通信的复杂性降低到它所属的服务器(例如过期标头、范围下载、缓存等)。 宁愿用脚本清理一些文件也不愿在我的应用程序中重新实现 HTTP。
  • 我同意威尔的观点。现在的浏览器都有复杂的下载器,可以使用 http 的内置功能暂停和恢复下载。
【解决方案2】:

当我过去这样做时,我使用过这个:

set_time_limit(0); //Set the execution time to infinite.
header('Content-Type: application/exe'); //This was for a LARGE exe (680MB) so the content type was application/exe
readfile($fileName); //readfile will stream the file.

这 3 行代码将完成下载的所有工作readfile() 会将指定的整个文件流式传输到客户端,并确保设置无限时间限制,否则您可能会在文件被用完之前耗尽时间完成流式传输。

【讨论】:

  • 当然,如果这不是exe,mime类型应该不同。
  • 另外,永远不要将 set_time_limit(0) 与 ignore_user_abort() 结合起来,否则脚本可能会永远运行。
  • 非常正确,除非您当然希望脚本在用户死后继续运行。
  • 这会留下你杀死用户的证据。不完全是一个好主意。
  • 这不适用于大文件。这个问题专门针对大型文件。投反对票。
【解决方案3】:

如果您要发送真正的大文件并担心这会产生影响,您可以使用 x-sendfile 标头。

来自 SOQ using-xsendfile-with-apache-php,howto blog.adaniels.nl : how-i-php-x-sendfile/

【讨论】:

  • 谢谢,我不知道那个。
  • 有一个 "large file download" PHP script here 可以处理大约 2GB 的许多文件类型,如 exe、mp3、mp4、pdf 等。这个脚本值得下载中小型文件大小通过 PHP 脚本。它还描述了 X-sendfile 用于下载超过 5GB 的真正海量文件。查看链接。
  • @webblover - 您的链接已失效。可以转发吗?
  • @Ben 对此感到抱歉。 'phpsnips' 网站已删除该 sn-p。你可以直接从 Github 下载:github.com/saleemkce/downloadable/blob/master/download.php
  • 大文件下载”脚本REPOSTED就在上面。
【解决方案4】:

如果您使用 lighttpd 作为网络服务器,安全下载的替代方法是使用 ModSecDownload。它需要服务器配置,但您将让网络服务器自己处理下载,而不是 PHP 脚本。

生成下载 URL 看起来像这样(取自文档),当然它只能为授权用户生成:

<?php

  $secret = "verysecret";
  $uri_prefix = "/dl/";

  # filename
  # please note file name starts with "/" 
  $f = "/secret-file.txt";

  # current timestamp
  $t = time();

  $t_hex = sprintf("%08x", $t);
  $m = md5($secret.$f.$t_hex);

  # generate link
  printf('<a href="%s%s/%s%s">%s</a>',
         $uri_prefix, $m, $t_hex, $f, $f);
?>

当然,根据文件的大小,使用readfile()(如Unkwntech 提出的)非常好。并且使用garrow 提出的 xsendfile 是另一个 Apache 也支持的好主意。

【讨论】:

    【解决方案5】:

    最好的解决方案是依赖 lighty 或 apache,但如果在 PHP 中,我会使用 PEAR's HTTP_Download(无需重新发明轮子等),具有一些不错的功能,例如:

    • 基本节流机制
    • 范围(部分下载和恢复)

    intro/usage docs

    【讨论】:

    • 感谢您指向 HTTP_Download,这正是我正在寻找的,并且在下载 CD 映像时完美运行。
    • 我不得不修复几个不推荐使用的引用错误和非静态方法的使用,但在 5 分钟内我就启动并运行了它,所以这并不难。可能是因为我有一个过时的梨库,但值得注意。
    • 这不是很好。 HTTP_Download 在 2020 年已过时,甚至无法在 php7 中运行,因为它使用 &amp;new 对象实例创建方法,这在 PHP 7 中会出错。我浪费了很多时间让 pear 工作并安装 HTTP_Download 扩展,只是为了查看包已弃用。
    【解决方案6】:

    我不确定这对于大文件是否是个好主意。如果您的下载脚本线程一直运行到用户完成下载,并且您正在运行 Apache 之类的东西,那么仅 50 个或更多的并发下载可能会使您的服务器崩溃,因为 Apache 并非旨在运行大量长时间运行的同时线程。当然我可能是错的,如果 apache 线程以某种方式终止并且下载在下载过程中位于某处的缓冲区中。

    【讨论】:

      【解决方案7】:

      创建一个指向实际文件的符号链接,并使下载链接指向该符号链接。然后,当用户单击 DL 链接时,他们将从真实文件中下载文件,但从符号链接中命名。创建符号链接需要几毫秒,这比尝试将文件复制到新名称并从那里下载要好。

      例如:

      <?php
      
      // validation code here
      
      $realFile = "Hidden_Zip_File.zip";
      $id = "UserID1234";
      
      if ($_COOKIE['authvalid'] == "true") {
          $newFile = sprintf("myzipfile_%s.zip", $id); //creates: myzipfile_UserID1234.zip
      
          system(sprintf('ln -s %s %s', $realFile, $newFile), $retval);
      
          if ($retval != 0) {
              die("Error getting download file.");
          }
      
          $dlLink = "/downloads/hiddenfiles/".$newFile;
      }
      
      // rest of code
      
      ?>
      
      <a href="<?php echo $dlLink; ?>Download File</a>
      

      这就是我所做的,因为 Go Daddy 在 2 分 30 秒左右后终止了脚本的运行......这可以防止该问题并隐藏实际文件。

      然后您可以设置 CRON 作业以定期删除符号链接....

      然后整个过程会将文件发送到浏览器,因为它不是脚本,所以运行多长时间并不重要。

      【讨论】:

        【解决方案8】:

        我使用了在 readfile 的 php 手册条目的 cmets 中找到的以下 sn-p:

        function _readfileChunked($filename, $retbytes=true) {
            $chunksize = 1*(1024*1024); // how many bytes per chunk
            $buffer = '';
            $cnt =0;
            // $handle = fopen($filename, 'rb');
            $handle = fopen($filename, 'rb');
            if ($handle === false) {
                return false;
            }
            while (!feof($handle)) {
                $buffer = fread($handle, $chunksize);
                echo $buffer;
                ob_flush();
                flush();
                if ($retbytes) {
                    $cnt += strlen($buffer);
                }
            }
            $status = fclose($handle);
            if ($retbytes && $status) {
                return $cnt; // return num. bytes delivered like readfile() does.
            }
            return $status;
        }
        

        【讨论】:

        • 另一个不必要的实现。请在stackoverflow.com/a/21936954/2864740 上查看我的 cmets 了解原因。
        • 这是不正确的。我是凭经验知道的。正如您在评论中所说,使用 ob_end_clean() 并没有解决我的问题,而分块解决方案却解决了问题。关键是 readfile 不允许下载接近或大于 PHP 内存限制的文件。这是一个有效的简单解决方案,因此,您不应低估它。
        【解决方案9】:

        我们已经在几个项目中使用它,到目前为止效果很好:

        /**
         * Copy a file's content to php://output.
         *
         * @param string $filename
         * @return void
         */
        protected function _output($filename)
        {
            $filesize = filesize($filename);
        
            $chunksize = 4096;
            if($filesize > $chunksize)
            {
                $srcStream = fopen($filename, 'rb');
                $dstStream = fopen('php://output', 'wb');
        
                $offset = 0;
                while(!feof($srcStream)) {
                    $offset += stream_copy_to_stream($srcStream, $dstStream, $chunksize, $offset);
                }
        
                fclose($dstStream);
                fclose($srcStream);   
            }
            else 
            {
                // stream_copy_to_stream behaves() strange when filesize > chunksize.
                // Seems to never hit the EOF.
                // On the other handside file_get_contents() is not scalable. 
                // Therefore we only use file_get_contents() on small files.
                echo file_get_contents($filename);
            }
        }
        

        【讨论】:

        • 我尝试了这个,但它似乎所做的只是将文本输出到屏幕上。我如何把它变成一个文件来下载?
        • 这是一个不同的话题。您必须设置适当的内容标头:Content-Type、Content-Length 和可能的 Content-Disposition。
        • 4KB 块不会效率低下吗?我至少会做 0.5MB
        【解决方案10】:
        header("Content-length:".filesize($filename));
        header('Content-Type: application/zip'); // ZIP file
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="downloadpackage.zip"');
        header('Content-Transfer-Encoding: binary');
        ob_end_clean();
        readfile($filename);
        exit();
        

        【讨论】:

        • 不适用于大文件。将在几秒钟内耗尽内存。
        • @Scottymeuk 为什么?请提供资源。
        • 这就是您需要做的所有事情。真的。不知道为什么这不被接受。 “关键”是使用 ob_end_clean(可以包装在 ob_get_level 中)来禁用输出缓冲区。
        【解决方案11】:

        我也遇到了同样的问题, 我的问题通过在开始会话之前添加这个解决了 session_cache_limiter('none');

        【讨论】:

          【解决方案12】:

          分块文件是 PHP 中最快/最简单的方法,如果您不能或不想使用更专业的东西,例如 cURLmod-xsendfile on Apache 或一些 dedicated script .

          $filename = $filePath.$filename;
          
          $chunksize = 5 * (1024 * 1024); //5 MB (= 5 242 880 bytes) per one chunk of file.
          
          if(file_exists($filename))
          {
              set_time_limit(300);
          
              $size = intval(sprintf("%u", filesize($filename)));
          
              header('Content-Type: application/octet-stream');
              header('Content-Transfer-Encoding: binary');
              header('Content-Length: '.$size);
              header('Content-Disposition: attachment;filename="'.basename($filename).'"');
          
              if($size > $chunksize)
              { 
                  $handle = fopen($filename, 'rb'); 
          
                  while (!feof($handle))
                  { 
                    print(@fread($handle, $chunksize));
          
                    ob_flush();
                    flush();
                  } 
          
                  fclose($handle); 
              }
              else readfile($path);
          
              exit;
          }
          else echo 'File "'.$filename.'" does not exist!';
          

          richnetapps.com / NeedBee 移植。在 readfile() 死亡的 200 MB 文件上进行了测试,即使最大允许内存限制设置为 1G,也就是下载文件大小的五倍。

          顺便说一句:我也在文件&gt;2GB 上进行了测试,但 PHP 只设法先写入文件的2GB,然后断开连接。与文件相关的函数(fopen、fread、fseek)使用 INT,因此您最终达到了2GB 的限制。在这种情况下,上述解决方案(即mod-xsendfile)似乎是唯一的选择。

          编辑让自己100%将您的文件保存在utf-8。如果您忽略它,下载的文件将被损坏。这是因为此解决方案使用print 将文件块推送到浏览器。

          【讨论】:

          • 这是一个完美的工作解决方案,在我的许多项目中使用和测试。所以,我认为,投反对票的人,甚至无法在评论中表达,为什么他/她投反对票,只是度过了糟糕的一天,或者甚至无法理解这个答案。两种情况都可惜……
          • @user2864740 也许它在你的 PHP 中是这样,但不是在我的 PHP 中,也不是在官方的 PHP 中。告诉我,在readfile doc 的确切位置,你看到任何关于分块的提及吗?在哪里?只提到了 user-defined readfile_chunked 函数,它的源代码看起来...几乎和上面的例子一模一样。在你陈述你的虚假声明之前检查一下!
          • 来自文档:“注意:readfile() 将不会出现任何内存问题,即使在发送大文件时,它自己也是如此。如果您遇到内存不足错误,请确保使用 ob_get_level() 关闭输出缓冲。”这意味着内部 readfile 是流式传输的(又名“分块”);并且任何流式方法的问题仍然是输出缓冲(如果处于活动状态)。
          • @GellieAnn 您指的是 7 岁的答案。我不再是 PHP 开发人员。 `header('Content-Type: application/octet-stream');` 和header('Content-Transfer-Encoding: binary'); 建议纯二进制连接,所以理论上以这种方式下载 ZIP 文件绝对没有问题。但我无法在实践中验证这一点。另请注意,ZIP 文件并不意味着大于 2 GB,PHP 的库可能无法处理更大的文件(许多 ZIP 客户端无法处理;其他使用一些奇怪的技巧来处理如此大的 ZIP 文件)。也许这就是问题所在。
          • @GellieAnn 在谈论 UTF-8 编码时,我很可能是在 7 年前谈论 .php 文件本身(带有上述代码)。与您要以这种方式下载的文件无关。下载的文件应该是二进制文件。二进制文件不能使用 UTF-8 等文本编码进行编码。
          【解决方案13】:

          这是在具有 256MB 内存限制的服务器上对大小为 200+ MB 的文件进行测试的。

          header('Content-Type: application/zip');
          header("Content-Disposition: attachment; filename=\"$file_name\"");
          set_time_limit(0);
          $file = @fopen($filePath, "rb");
          while(!feof($file)) {
            print(@fread($file, 1024*8));
            ob_flush();
            flush();
          }
          

          【讨论】:

            猜你喜欢
            • 2011-02-06
            • 2015-09-08
            • 1970-01-01
            • 1970-01-01
            • 2015-01-12
            • 1970-01-01
            • 1970-01-01
            • 2011-08-06
            相关资源
            最近更新 更多