2009-09-09 4 views
9

J'essaie de flux/pipe d'un fichier dans le navigateur de l'utilisateur via HTTP à partir de FTP. Autrement dit, j'essaie d'imprimer le contenu d'un fichier sur un serveur FTP.Stream FTP téléchargement à la sortie

C'est ce que j'ai jusqu'à présent:

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 ressemble à ceci:

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

Avec cette approche, je ne suis en mesure d'envoyer de petits fichiers au navigateur de l'utilisateur. Pour des fichiers plus volumineux, rien est imprimé et je reçois une erreur fatale (lisible à partir des journaux Apache):

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

J'ai essayé de remplacer php://output avec php://stdout sans succès (rien ne semble être envoyé au navigateur).

Comment puis-je télécharger efficacement à partir du FTP tout en envoyant ces données au navigateur en même temps?

Remarque: Je ne voudrais pas utiliser file_get_contents('ftp://user:[email protected]:port/path/to/file'); ou similaire.

+0

Je serais vraiment intéressé par cette réponse aussi! – knittl

Répondre

8

trouvé une solution!

Créer une paire de sockets (tube anonyme?). Utilisez la fonction ftp_nb_fget non bloquante pour écrire à une extrémité du tube et echo à l'autre extrémité du tube.

Testé pour être rapide (facilement 10 Mo/s sur une connexion 100 Mbps) donc il n'y a pas beaucoup d'en-tête d'E/S.

Veillez à effacer les tampons de sortie. Frameworks généralement tampon votre sortie.

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

Remarque: cela nécessite que le tampon de contenu soit désactivé pour fonctionner. – LiraNuna

+0

+1 pour vraiment bonne réponse. Pour ceux qui se posent des questions sur toutes les constantes de 'stream_socket_pair', regardez ici: http://php.net/manual/fr/stream.constants.php –

1

On dirait que vous devez désactiver la mémoire tampon de sortie pour cette page, sinon PHP essayera de l'insérer dans toute la mémoire.

Un moyen facile de le faire est quelque chose comme:

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

Mettez cette avance de votre appel à -> get(), et je pense que cela va résoudre votre problème.

+0

J'ai dû utiliser 'while (ob_get_length()) ob_end_clean();' à la place pour l'exécuter. Cependant, je reçois toujours l'erreur fatale décrite dans le PO. – strager

0

(je ne l'ai jamais rencontré ce problème moi-même, de sorte que c'est juste une supposition sauvage, mais, peut-être ...)

peut-être changer la taille de la mémoire tampon de ouput pour le « fichier » vous écrivez pourrait aider?

Pour cela, voir stream_set_write_buffer.

Par exemple:

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

Avec cela, votre code devrait utiliser un flux non-tampon - ce qui pourrait aider ...

+0

Cela semble être une bonne solution, mais cela n'a pas fonctionné. – strager

5

Essayez:

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

Je trouve avec beaucoup d'opérations sur les fichiers, cela vaut la peine de laisser la fonctionnalité sous-jacente du système d'exploitation prendre soin de vous.

+2

Est-il possible d'échapper le nom d'utilisateur et le mot de passe, donc s'ils contiennent des caractères tels que '@' ou '/', ils seront lus correctement? – strager

1

je sais que c'est vieux, mais certains peuvent penser encore il est utile.

J'ai essayé votre solution sur un environnement Windows, et cela a fonctionné presque parfaitement:

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

J'utilisé STREAM_PF_INET au lieu de STREAM_PF_UNIX à cause de Windows, et cela a fonctionné parfaitement ... jusqu'à ce que le dernier morceau, qui était false sans raison apparente, et je ne pouvais pas comprendre pourquoi. Donc, la sortie manquait la dernière partie.

Je décide donc d'utiliser une autre approche:

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

Cela a fonctionné comme un charme avec PHP 5.4.5. La mauvaise partie est que vous ne pouvez pas attraper les données transférées, seulement la taille du morceau.