Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stream remote file with PHP and Guzzle

My application should stream back to the browser a large file, that is server remotely. Currently the file is served from a local NodeJS server.

I am using a VirtualBox Disk image of 25GB, just to be sure that it is not stored in memory while streaming. This is the related code I'm struggling with

    require __DIR__ . '/vendor/autoload.php';
    use GuzzleHttp\Stream\Stream;
    use GuzzleHttp\Stream\LimitStream;

    $client = new \GuzzleHttp\Client();
    logger('==== START REQUEST ====');
    $res = $client->request('GET', 'http://localhost:3002/', [
      'on_headers' => function (\Psr\Http\Message\ResponseInterface $response) use ($res) {
        $length = $response->getHeaderLine('Content-Length');
        logger('Content length is: ' . $length);
        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="testfile.zip"');
        header('Expires: 0');
        header('Cache-Control: must-revalidate');
        header('Pragma: public');
        header('Content-Length: ' . $length);

      }
    ]);

    $body = $res->getBody();
    $read = 0;
    while(!$body->eof()) {
      logger("Reading chunk. " . $read);
      $chunk = $body->read(8192);
      $read += strlen($chunk);
      echo $chunk;
    }
    logger('Read ' . $read . ' bytes');
    logger("==== END REQUEST ====\n\n");

    function logger($string) {
      $myfile = fopen("log.txt", "a") or die ('Unable to open log file');
      fwrite($myfile, "[" . date("d/m/Y H:i:s") . "] " . $string . "\n");
      fclose($myfile);
    }

Even though $body = $res->getBody(); should return a stream, it quickly full the disk with swap data, meaning that it is trying to save that in memory before streaming back to the client, but this is not the expected behavior. What am I missing?

like image 873
Leonardo Rossi Avatar asked Feb 06 '23 11:02

Leonardo Rossi


1 Answers

You have to specify stream and sink options like this:

$res = $client->request('GET', 'http://localhost:3002/', [
    'stream' => true,
    'sink' => STDOUT, // Default output stream.
    'on_headers' => ...
]);

After these additions you will be able to stream responses chunk by chunk, without any additional code to copy from response body stream to STDOUT (with echo).

But usually you don't want to do this, because you will need to have one process of PHP (php-fpm or Apache's mod_php) for each active client.

If you just want to serve secret files, try to use an "internal redirect": through X-Accel-Redirect header for nginx or X-Sendfile for Apache. You will get the same behavior, but with less resource usage (because of high optimized event loop in case of nginx). For configuration details you can read an official documentation or, of course, other SO questions (like this one).

like image 168
Alexey Shokov Avatar answered Feb 09 '23 22:02

Alexey Shokov