2009-09-09 13 views
9

Estoy tratando de transmitir/canalizar un archivo al navegador del usuario a través de HTTP desde FTP. Es decir, estoy tratando de imprimir el contenido de un archivo en un servidor FTP.Transmitir la descarga de FTP a la salida

Esto es lo que tengo hasta ahora:

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 se parece a esto:

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

Con este enfoque, sólo soy capaz de enviar archivos pequeños al navegador del usuario. Para archivos más grandes, nada se imprime y se me sale un error fatal (legible desde los registros de Apache):

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

he intentado reemplazar php://output con php://stdout sin éxito (nada parece ser enviados al navegador).

¿Cómo puedo descargar de manera eficiente desde FTP mientras envío esos datos al navegador al mismo tiempo?

Nota: No me gustaría usar file_get_contents('ftp://user:[email protected]:port/path/to/file'); o similar.

+0

¡Estaría realmente interesado en esta respuesta también! – knittl

Respuesta

8

encontrado una solución!

Crear un par de socket (tubo anónimo?). Use la función ftp_nb_fget sin bloqueo para escribir en un extremo del tubo y echo en el otro extremo del tubo.

Probado para ser rápido (fácilmente 10MB/s en una conexión de 100Mbps) por lo que no hay mucha sobrecarga de E/S.

Asegúrese de borrar los búferes de salida. Los marcos normalmente almacenan en búfer tu salida.

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

Nota: esto necesita que el almacenamiento en memoria intermedia de contenido esté desactivado para funcionar. – LiraNuna

+0

+1 para una respuesta realmente buena. Para aquellos que se preguntan por todas las constantes en 'stream_socket_pair', échenle un vistazo aquí: http://php.net/manual/en/stream.constants.php –

1

Parece que tiene que desactivar el almacenamiento en búfer de salida para esa página, de lo contrario, PHP intentará incluirlo en toda la memoria.

Una manera fácil de hacer esto es algo como:

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

poner eso por delante de su llamada a -> get(), y yo creo que va a resolver su problema.

+0

Tuve que usar 'while (ob_get_length()) ob_end_clean();' en su lugar para que se ejecutara. Sin embargo, sigo teniendo el error fatal descrito en el PO. – strager

0

(nunca he conocido a este problema a mí mismo, así que eso es sólo una suposición, pero, tal vez ...)

cambiando Tal vez el tamaño de la memoria intermedia ouput para el "archivo" que está escribiendo para ayudar?

Para eso, vea stream_set_write_buffer.

Por ejemplo:

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

Con esto, el código debe utilizar una corriente no amortiguada - esto podría ayudar ...

+0

Parece una buena solución, pero no funcionó. – strager

5

Probar:

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

encuentro con una gran cantidad de operaciones de archivos, vale la pena dejar que la funcionalidad del sistema operativo subyacente se encargue de ello.

+2

¿Hay alguna forma de evitar el nombre de usuario y la contraseña, de modo que si contienen caracteres como '@' o '/' se leerán correctamente? – strager

1

Sé que esto es viejo, pero algunos todavía puede pensar que es útil.

que he probado su solución en un entorno Windows, y funcionó casi a la perfección:

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

que utilizan STREAM_PF_INET en lugar de STREAM_PF_UNIX debido a Windows, y funcionó a la perfección hasta el último ... pedazo, que era false sin razón aparente, y no pude entender por qué. Entonces, a la salida le faltaba la última parte.

así que decidí utilizar otro enfoque:

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

Esto funcionó como un encanto con PHP 5.4.5. Lo malo es que no puedes capturar los datos transferidos, solo el tamaño del fragmento.

Cuestiones relacionadas