2009-09-09 9 views
9

Ich versuche, eine Datei über HTTP vom FTP-Server zum Browser des Benutzers zu streamen/zu pipen. Das heißt, ich versuche, den Inhalt einer Datei auf einem FTP-Server zu drucken.Stream FTP-Download zur Ausgabe

Dies ist, was ich bisher:

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 sieht wie folgt aus:

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

Mit diesem Ansatz, ich bin nur in der Lage kleine Dateien an den Browser des Benutzers zu senden. Für größere Dateien, wird nichts gedruckt, und ich bekomme einen fatalen Fehler (lesbar von Apache-Logs):

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

versuchte ich php://output mit php://stdout ohne Erfolg zu ersetzen (es scheint nichts an den Browser gesendet zu werden).

Wie kann ich effizient von FTP herunterladen, während ich diese Daten gleichzeitig an den Browser sende?

Hinweis: Ich möchte nicht file_get_contents('ftp://user:[email protected]:port/path/to/file'); oder ähnliches verwenden.

+0

ich würde wirklich an dieser Antwort auch interessiert sein! – knittl

Antwort

8

eine Lösung gefunden!

Erstellen Sie ein Socket-Paar (anonyme Leitung?). Verwenden Sie die nicht blockierende ftp_nb_fget-Funktion, um an ein Ende der Rohrleitung zu schreiben, und echo das andere Ende der Rohrleitung.

Getestet zu schnell (leicht 10MB/s auf einer 100Mbps-Verbindung), so gibt es nicht viel I/O Overhead.

Achten Sie darauf, alle Ausgabepuffer zu löschen. Frameworks puffern häufig Ihre Ausgabe.

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

Hinweis: Dies erfordert Content-Pufferung auf OFF zu arbeiten. – LiraNuna

+0

+1 für wirklich gute Antwort. Für diejenigen, die sich über alle Konstanten in 'stream_socket_pair' wundern, schauen Sie hier: http://php.net/manual/en/stream.constants.php –

1

Klingt, als müssten Sie die Ausgabepufferung für diese Seite ausschalten, sonst wird PHP versuchen, sie in den gesamten Speicher zu packen.

Eine einfache Möglichkeit, dies zu tun, ist so etwas wie:

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

Put, die vor Ihrem Anruf -> get(), und ich denke, dass Ihr Problem behoben wird.

+0

Ich musste 'while (ob_get_length()) ob_end_clean();' stattdessen verwenden, um es zum Laufen zu bekommen. Allerdings bekomme ich immer noch den im OP beschriebenen fatalen Fehler. – strager

0

(Ich habe noch nie dieses Problem selbst getroffen, so dass nur eine wilde Vermutung ist, aber vielleicht ...)

Vielleicht die Größe des ouput Puffers ändert für die „Datei“ Sie schreiben könnte helfen?

Dafür siehe stream_set_write_buffer.

Zum Beispiel:

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

Damit Ihr Code sollte einen ungepufferten Stream verwenden - dies helfen könnte ...

+0

Scheint wie eine gute Lösung, aber es hat nicht funktioniert. – strager

5

Versuchen:

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

Ich finde mit Bei vielen Dateioperationen lohnt es sich, die zugrundeliegende Betriebssystemfunktionalität für sich zu übernehmen.

+2

Gibt es eine Möglichkeit, den Benutzernamen und das Passwort zu umgehen, wenn sie Zeichen wie '@' oder '/' enthalten, wird es richtig gelesen? – strager

1

Ich weiß, das alt ist, aber einige können immer noch denken, es nützlich ist.

Ich habe Ihre Lösung auf einer Windows-Umgebung versucht, und es hat funktioniert fast perfekt:

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

ich STREAM_PF_INET statt STREAM_PF_UNIX weil von Windows verwendet, und es funktionierte einwandfrei ... bis zum letzten Chunk, die false ohne ersichtlichen Grund war, und ich konnte nicht verstehen, warum. So fehlte der Ausgabe der letzte Teil.

Also beschloss ich, einen anderen Ansatz zu verwenden:

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

Dieses wie ein Zauber mit PHP 5.4.5 gearbeitet. Der schlechte Teil ist, dass Sie die übertragenen Daten nicht abfangen können, nur die Chunk-Größe.

Verwandte Themen