/**
  * Reads multipart data from sourceConnection and streams it to the
  * targetConnection.Returns the body of the request or the status code in
  * case there is no body.
  *
  * @param $method
  * @param $path
  * @param $streamEnd
  * @param array $requestHeaders
  * @return mixed|string
  * @throws \Exception
  * @throws \HTTPException
  */
 protected function sendStream($method, $path, $streamEnd, $requestHeaders = array())
 {
     $dataStream = $this->sourceConnection;
     // Read the json doc. Use _attachments field to find the total
     // Content-Length and create the request header with initial doc data.
     // At present CouchDB can't handle chunked data and needs
     // Content-Length header.
     $str = '';
     $jsonFlag = 0;
     $attachmentCount = 0;
     $totalAttachmentLength = 0;
     $streamLine = $this->getNextLineFromSourceConnection();
     while ($jsonFlag == 0 || $jsonFlag == 1 && trim($streamLine) == '') {
         $str .= $streamLine;
         if (strpos($streamLine, 'Content-Type: application/json') !== false) {
             $jsonFlag = 1;
         }
         $streamLine = $this->getNextLineFromSourceConnection();
     }
     $docBoundaryLength = strlen(explode('=', $requestHeaders['Content-Type'], 2)[1]) + 2;
     $json = json_decode($streamLine, true);
     foreach ($json['_attachments'] as $docName => $metaData) {
         // Quotes and a "/r/n"
         $totalAttachmentLength += strlen('Content-Disposition: attachment; filename=') + strlen($docName) + 4;
         $totalAttachmentLength += strlen('Content-Type: ') + strlen($metaData['content_type']) + 2;
         $totalAttachmentLength += strlen('Content-Length: ');
         if (isset($metaData['encoding'])) {
             $totalAttachmentLength += $metaData['encoded_length'] + strlen($metaData['encoded_length']) + 2;
             $totalAttachmentLength += strlen('Content-Encoding: ') + strlen($metaData['encoding']) + 2;
         } else {
             $totalAttachmentLength += $metaData['length'] + strlen($metaData['length']) + 2;
         }
         $totalAttachmentLength += 2;
         $attachmentCount++;
     }
     // Add Content-Length to the headers.
     $requestHeaders['Content-Length'] = strlen($str) + strlen($streamLine) + $totalAttachmentLength + $attachmentCount * (2 + $docBoundaryLength) + $docBoundaryLength + 2;
     if ($this->targetConnection == null) {
         $this->targetConnection = $this->targetClient->getConnection($method, $path, null, $requestHeaders);
     }
     // Write the initial body data.
     fwrite($this->targetConnection, $str);
     // Write the rest of the data including attachments line by line or in
     // chunks.
     while (!feof($dataStream) && ($streamEnd === null || strpos($streamLine, $streamEnd) === false)) {
         $totalSent = 0;
         $length = strlen($streamLine);
         while ($totalSent != $length) {
             $sent = fwrite($this->targetConnection, substr($streamLine, $totalSent));
             if ($sent === false) {
                 throw new \HTTPException('Stream write error.');
             } else {
                 $totalSent += $sent;
             }
         }
         // Use maxLength while reading the data as there may be no newlines
         // in the binary and compressed attachments, or the lines may be
         // very long.
         $streamLine = $this->getNextLineFromSourceConnection(100000);
     }
     // Read response headers
     $rawHeaders = '';
     $headers = array('connection' => $this->targetClient->getOptions()['keep-alive'] ? 'Keep-Alive' : 'Close');
     // Remove leading newlines, should not occur at all, actually.
     while (($line = fgets($this->targetConnection)) !== false && ($lineContent = rtrim($line)) === '') {
     }
     // Throw exception, if connection has been aborted by the server, and
     // leave handling to the user for now.
     if ($line === false) {
         // sendStream can't be called in recursion as the source stream can be
         // read only once.
         $error = error_get_last();
         throw HTTPException::connectionFailure($this->targetClient->getOptions()['ip'], $this->targetClient->getOptions()['port'], $error['message'], 0);
     }
     do {
         // Also store raw headers for later logging
         $rawHeaders .= $lineContent . "\n";
         // Extract header values
         if (preg_match('(^HTTP/(?P<version>\\d+\\.\\d+)\\s+(?P<status>\\d+))S', $lineContent, $match)) {
             $headers['version'] = $match['version'];
             $headers['status'] = (int) $match['status'];
         } else {
             list($key, $value) = explode(':', $lineContent, 2);
             $headers[strtolower($key)] = ltrim($value);
         }
     } while (($line = fgets($this->targetConnection)) !== false && ($lineContent = rtrim($line)) !== '');
     // Read response body
     $body = '';
     // HTTP 1.1 supports chunked transfer encoding, if the according
     // header is not set, just read the specified amount of bytes.
     $bytesToRead = (int) (isset($headers['content-length']) ? $headers['content-length'] : 0);
     // Read body only as specified by chunk sizes, everything else
     // are just footnotes, which are not relevant for us.
     while ($bytesToRead > 0) {
         $body .= $read = fgets($this->targetConnection, $bytesToRead + 1);
         $bytesToRead -= strlen($read);
     }
     // Reset the connection if the server asks for it.
     if ($headers['connection'] !== 'Keep-Alive') {
         fclose($this->targetConnection);
         $this->targetConnection = null;
     }
     // Handle some response state as special cases
     switch ($headers['status']) {
         case 301:
         case 302:
         case 303:
         case 307:
             // Temporary redirect.
             // sendStream can't be called in recursion as the source stream can be
             // read only once.
             throw HTTPException::fromResponse($path, new Response($headers['status'], $headers, $body));
     }
     return $body != '' ? json_decode($body, true) : array("status" => $headers['status']);
 }