Я пытаюсь передать / передать файл в браузер пользователя через HTTP с FTP. То есть, я пытаюсь распечатать содержимое файла на FTP-сервере.
Это то, что у меня есть до сих пор:
public function echo_contents() { $file = fopen('php://output', 'w+'); if(!$file) { throw new Exception('Unable to open output'); } try { $this->ftp->get($this->path, $file); } catch(Exception $e) { fclose($file); // wtb finally throw $e; } fclose($file); }
$this->ftp->get
выглядит так:
public function get($path, $stream) { ftp_fget($this->ftp, $stream, $path, FTP_BINARY); // Line 200 }
При таком подходе я могу отправлять небольшие файлы в браузер пользователя. Для больших файлов ничего не печатается, и я получаю фатальную ошибку (читается из журналов Apache):
PHP Неустранимая ошибка: допустимый размер памяти 16777216 байт исчерпан (пытался выделить 15994881 байт) в /xxx/ftpconnection.php в строке 200
Я попытался заменить php://output
с помощью php://stdout
без успеха (в браузере ничего не отправлено).
Как я могу эффективно загружать с FTP при одновременном отправке этих данных в браузер?
Примечание. Я бы не хотел использовать file_get_contents('ftp://user:pass@host:port/path/to/file');
или похожие.
Нашли решение!
Создайте пару сокетов (анонимный канал?). Используйте функцию non-blocking ftp_nb_fget
для записи на один конец канала и echo
другого конца канала.
Протестировано, чтобы быть быстрым (легко 10 МБ / с при соединении 100 Мбит / с), поэтому накладных расходов ввода-вывода не так много.
Обязательно очистите все выходные буферы. Рамки обычно буферизуют ваш вывод.
public function echo_contents() { /* FTP writes to [0]. Data passed through from [1]. */ $sockets = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); if($sockets === FALSE) { throw new Exception('Unable to create socket pair'); } stream_set_write_buffer($sockets[0], 0); stream_set_timeout($sockets[1], 0); try { // $this->ftp is an FtpConnection $get = $this->ftp->get_non_blocking($this->path, $sockets[0]); while(!$get->is_finished()) { $contents = stream_get_contents($sockets[1]); if($contents !== false) { echo $contents; flush(); } $get->resume(); } $contents = stream_get_contents($sockets[1]); if($contents !== false) { echo $contents; flush(); } } catch(Exception $e) { fclose($sockets[0]); // wtb finally fclose($sockets[1]); throw $e; } fclose($sockets[0]); fclose($sockets[1]); } // class FtpConnection public function get_non_blocking($path, $stream) { // $this->ftp is the FTP resource returned by ftp_connect return new FtpNonBlockingRequest($this->ftp, $path, $stream); } /* TODO Error handling. */ class FtpNonBlockingRequest { protected $ftp = NULL; protected $status = NULL; public function __construct($ftp, $path, $stream) { $this->ftp = $ftp; $this->status = ftp_nb_fget($this->ftp, $stream, $path, FTP_BINARY); } public function is_finished() { return $this->status !== FTP_MOREDATA; } public function resume() { if($this->is_finished()) { throw BadMethodCallException('Cannot continue download; already finished'); } $this->status = ftp_nb_continue($this->ftp); } }
Пытаться:
@readfile('ftp://username:password@host/path/file'));
Я нахожу с большим количеством операций с файлами, поэтому стоит использовать базовую функциональность ОС, заботясь об этом для вас.
Похоже, вам нужно отключить буферизацию вывода для этой страницы, иначе PHP попытается поместить ее во всю память.
Легкий способ сделать это:
while (ob_end_clean()) { ; # do nothing }
Положите, что перед вашим вызовом -> get (), и я думаю, что это решит вашу проблему.
Я знаю, что это старо, но некоторые могут по-прежнему считать, что это полезно.
Я пробовал ваше решение в среде Windows, и он работал почти отлично:
$conn_id = ftp_connect($host); ftp_login($conn_id, $user, $pass) or die(); $sockets = stream_socket_pair(STREAM_PF_INET, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP) or die(); stream_set_write_buffer($sockets[0], 0); stream_set_timeout($sockets[1], 0); set_time_limit(0); $status = ftp_nb_fget($conn_id, $sockets[0], $filename, FTP_BINARY); while ($status === FTP_MOREDATA) { echo stream_get_contents($sockets[1]); flush(); $status = ftp_nb_continue($conn_id); } echo stream_get_contents($sockets[1]); flush(); fclose($sockets[0]); fclose($sockets[1]);
Я использовал STREAM_PF_INET
вместо STREAM_PF_UNIX
из-за Windows, и он работал безупречно … до последнего фрагмента, который был false
без видимой причины, и я не мог понять, почему. Таким образом, на выходе отсутствовала последняя часть.
Поэтому я решил использовать другой подход:
$ctx = stream_context_create(); stream_context_set_params($ctx, array('notification' => function($code, $sev, $message, $msgcode, $bytes, $length) { switch ($code) { case STREAM_NOTIFY_CONNECT: // Connection estabilished break; case STREAM_NOTIFY_FILE_SIZE_IS: // Getting file size break; case STREAM_NOTIFY_PROGRESS: // Some bytes were transferred break; default: break; } })); @readfile("ftp://$user:$pass@$host/$filename", false, $ctx);
Это работало как шарм с PHP 5.4.5. Плохая часть заключается в том, что вы не можете поймать переданные данные, а только размер блока.
быстрый поиск поднял флеш-флеш .
эта статья может также представлять интерес: http://www.net2ftp.org/forums/viewtopic.php?id=3774
(Я никогда не встречал эту проблему сам, так что это просто дикая догадка, но, может быть …)
Может быть, изменить размер буфера ouput для «файла», который вы пишете, может помочь?
Для этого см. stream_set_write_buffer
.
Например :
$fp = fopen('php://output', 'w+'); stream_set_write_buffer($fp, 0);
При этом ваш код должен использовать небуферизованный поток – это может помочь …