Надежная загрузка больших файлов в PHP

У меня есть php-скрипт на сервере для отправки файлов получателям: они получают уникальную ссылку, а затем могут загружать большие файлы. Иногда возникает проблема с передачей, и файл поврежден или никогда не завершается. Мне интересно, есть ли лучший способ отправлять большие файлы

Код:

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

Я видел такие функции, как

http_send_file
http_send_data

Но я не уверен, что они сработают.

Как лучше всего решить эту проблему?

С уважением,
erwing

Ответов (13)

Я использовал следующий фрагмент, найденный в комментариях к ручной записи php для readfile:

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;
}
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();

У меня была такая же проблема, моя проблема решена добавлением этого перед запуском сеанса session_cache_limiter ('none');

Создайте символическую ссылку на фактический файл и сделайте ссылку для скачивания на символическую ссылку. Затем, когда пользователь щелкает ссылку 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 для удаления символических ссылок через определенные промежутки времени ....

Затем весь этот процесс отправит файл в браузер, и не имеет значения, как долго он выполняется, поскольку это не скрипт.

Мы использовали это в нескольких проектах, и пока он работает достаточно хорошо:

/**
 * 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);
    }
}

Разделение файлов на части - это самый быстрый / простой метод в PHP, если вы не можете или не хотите использовать что-то более профессиональное, например cURL , mod-xsendfileна Apache или какой-либо специальный скрипт .

$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 . Проверено на файлах размером 200 МБ, которые readfile() умерли, даже при установленном максимально допустимом ограничении памяти 1G, что в пять раз больше, чем размер загруженного файла.

Кстати: я тестировал это также на файлах >2GB, но PHP только успел записать сначала 2GB файл, а затем разорвал соединение. Файловые функции (fopen, fread, fseek) используют INT, поэтому вы в конечном итоге достигли предела 2GB . Вышеупомянутые решения (т.е. mod-xsendfile ) кажутся единственным вариантом в этом случае.

РЕДАКТИРОВАТЬ : Убедитесь, что ваш файл сохранен на 100%utf-8 . Если вы его не укажете, загруженные файлы будут повреждены. Это связано с тем, что это решение используется printдля отправки фрагмента файла в браузер.

Если вы отправляете действительно большие файлы и беспокоитесь о том, какое влияние это окажет, вы можете использовать заголовок x-sendfile.

Из SOQ using-xsendfile-with-apache-php можно найти howto blog.adaniels.nl: how-i-php-x-sendfile /

Если вы используете 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 , отлично. А использование xsendfile, предложенного garrow, - еще одна хорошая идея, также поддерживаемая Apache.

Это проверено на файлах размером 200+ МБ на сервере с ограничением памяти 256 МБ.

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();
}

Самый простой способ загрузки файлов, который я могу придумать, - это поместить файл во временное место и дать им уникальный URL-адрес, который они могут загрузить через обычный HTTP.

При создании этих ссылок вы также можете удалить файлы, возраст которых превышает X часов.

Когда я делал это в прошлом, я использовал это:

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 () будет передавать весь файл, указанный клиенту, и обязательно установить бесконечный лимит времени, иначе у вас может не хватить времени до того, как файл будет завершен потоковой передачей.

Лучшим решением было бы полагаться на lighty или apache, но если бы в PHP, я бы использовал PEAR HTTP_Download (не нужно изобретать колесо и т. Д.), Имеет некоторые приятные функции, например:

  • Базовый механизм дросселирования
  • Диапазоны (частичная загрузка и возобновление)

См. Вводную / документацию по использованию .

Я не уверен, что это хорошая идея для больших файлов. Если поток для вашего сценария загрузки выполняется до тех пор, пока пользователь не завершит загрузку, а вы используете что-то вроде Apache, всего 50 или более одновременных загрузок могут привести к сбою вашего сервера, потому что Apache не предназначен для запуска большого количества длительно выполняемых потоки одновременно. Конечно, я могу ошибаться, если поток apache каким-то образом завершается и загрузка находится где-то в буфере, пока загрузка продолжается.