2009-09-09 13 views
9

Próbuję przesyłać strumieniowo/potokować plik do przeglądarki użytkownika poprzez HTTP z FTP. To znaczy, próbuję wydrukować zawartość pliku na serwerze FTP.Strumień FTP do pobrania na wyjście

To, co mam tak daleko:

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 wygląda następująco:

public function get($path, $stream) { 
    ftp_fget($this->ftp, $stream, $path, FTP_BINARY); // Line 200 
} 

Dzięki takiemu podejściu, jestem w stanie jedynie do wysyłania małych plików do przeglądarki użytkownika. W przypadku większych plików, nic nie zostanie wydrukowany i otrzymuję błąd krytyczny (odczytać z logów Apache):

PHP Fatal error: Allowed memory size of 16777216 bytes exhausted (tried to allocate 15994881 bytes) in /xxx/ftpconnection.php on line 200

Próbowałem zastępując php://output z php://stdout bez powodzenia (nic nie wydaje się być wysyłane do przeglądarki).

Jak skutecznie pobierać z serwera FTP podczas wysyłania tych danych do przeglądarki w tym samym czasie?

Uwaga: Nie chciałbym używać file_get_contents('ftp://user:[email protected]:port/path/to/file'); lub podobnego.

+0

Byłbym też bardzo zainteresowany tą odpowiedzią! – knittl

Odpowiedz

8

znalazł rozwiązanie!

Utwórz parę gniazd (anonimowy potok?). Użyj nieblokującej funkcji ftp_nb_fget, aby pisać na jednym końcu rury, a echo na drugim końcu rury.

Testowany pod kątem szybki (łatwo 10 MB/s na połączenie 100 Mb/s), więc nie ma zbyt wiele operacji we/wy.

Upewnij się, że wyczyściłeś bufory wyjściowe. Ramy zwykle buforują twoje wyniki.

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); 
    } 
} 
+0

Uwaga: wymaga to, aby buforowanie zawartości było WYŁĄCZONE do pracy. – LiraNuna

+0

+1 za naprawdę dobrą odpowiedź. Dla tych, którzy zastanawiają się nad wszystkimi stałymi w 'stream_socket_pair', zajrzyj tutaj: http://php.net/manual/en/stream.constants.php –

1

Wygląda na to, że należy wyłączyć buforowanie wyjściowe dla tej strony, w przeciwnym razie PHP spróbuje dopasować ją do całej pamięci.

Łatwym sposobem na to jest coś takiego:

while (ob_end_clean()) { 
    ; # do nothing 
} 

umieścić, że przed swoim powołaniu do -> get() i myślę, że będzie rozwiązać problem.

+0

Musiałem użyć 'while (ob_get_length()) ob_end_clean();' zamiast tego, aby uruchomić go. Jednak nadal otrzymuję błąd krytyczny opisany w PO. – strager

0

(Nigdy nie spotkałem się ten problem, tak, że tylko dzikie przypuszczenie, ale może ...)

Może zmieniając rozmiar bufora Ouput dla „plik” piszesz może pomóc?

W tym celu zobacz stream_set_write_buffer.

Na przykład:

$fp = fopen('php://output', 'w+'); 
stream_set_write_buffer($fp, 0); 

z tym, kod powinien używać non-buforowane Stream - to może pomóc ...

+0

Wydaje się, że to dobre rozwiązanie, ale nie zadziałało. – strager

5

Spróbuj:

@readfile('ftp://username:[email protected]/path/file')); 

znajdę z wiele operacji na plikach warto pozwolić, aby podstawowa funkcjonalność systemu operacyjnego zajęła się nim.

+2

Czy istnieje sposób na ucieczkę od nazwy użytkownika i hasła, więc jeśli zawierają znaki takie jak "@" lub "/", to będzie poprawnie czytać? – strager

1

Wiem, że jest stary, ale niektórzy mogą nadal uważać, że jest przydatny.

Próbowałem swoje rozwiązanie w środowisku Windows, i to działało prawie doskonale:

$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]); 

użyłem STREAM_PF_INET zamiast STREAM_PF_UNIX ponieważ Windows, i to działało bez zarzutu ... aż do ostatniego fragment, który był false bez wyraźnego powodu, i nie mogłem zrozumieć, dlaczego. Tak więc w wyniku brakowało ostatniej części.

Postanowiłem więc użyć innego podejścia:

$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:[email protected]$host/$filename", false, $ctx); 

To działało jak czar z PHP 5.4.5. Złą stroną jest to, że nie można przechwycić przesłanych danych, a jedynie wielkość porcji.