mirror of
https://github.com/guzzle/guzzle.git
synced 2025-02-24 18:13:00 +01:00
Improving the CachingEntityBody
Fixing the stream size of a caching entity body to allow the size to be the greater of the buffer or remote stream. Fixing __toString() to properly combine the buffer and remote stream Skipping bytes written over the remote stream to emulate how other r/w PHP streams work (you overwrite bytes, you don't insert bytes and push out things after it). Using fstat() for local streams rather than loading streams into memory and getting the strlen() (previously used for temp streams).
This commit is contained in:
parent
1e0a4c1b3e
commit
cfd7f672e8
@ -92,7 +92,7 @@ class AbstractEntityBodyDecorator implements EntityBodyInterface
|
||||
*/
|
||||
public function getContentLength()
|
||||
{
|
||||
return $this->body->getContentLength();
|
||||
return $this->getSize();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -14,6 +14,11 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
*/
|
||||
protected $remoteStream;
|
||||
|
||||
/**
|
||||
* @var int The number of bytes to skip reading due to a write on the temporary buffer
|
||||
*/
|
||||
protected $skipReadBytes = 0;
|
||||
|
||||
/**
|
||||
* We will treat the buffer object as the body of the entity body
|
||||
* {@inheritdoc}
|
||||
@ -22,11 +27,6 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
{
|
||||
$this->remoteStream = $body;
|
||||
$this->body = new EntityBody(fopen('php://temp', 'r+'));
|
||||
// Use the specifically set size of the remote stream if it is available
|
||||
$remoteSize = $this->remoteStream->getSize();
|
||||
if (false !== $remoteSize) {
|
||||
$this->body->setSize($remoteSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -38,14 +38,27 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
$str = (string) $this->body;
|
||||
while (!$this->remoteStream->feof()) {
|
||||
$str .= $this->remoteStream->read(16384);
|
||||
$pos = $this->ftell();
|
||||
$this->rewind();
|
||||
|
||||
$str = '';
|
||||
while (!$this->isConsumed()) {
|
||||
$str .= $this->read(16384);
|
||||
}
|
||||
|
||||
$this->seek($pos);
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSize()
|
||||
{
|
||||
return max($this->body->getSize(), $this->remoteStream->getSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
* @throws RuntimeException When seeking with SEEK_END or when seeking past the total size of the buffer stream
|
||||
@ -61,9 +74,9 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
}
|
||||
|
||||
// You cannot skip ahead past where you've read from the remote stream
|
||||
if ($byte > $this->getSize()) {
|
||||
if ($byte > $this->body->getSize()) {
|
||||
throw new RuntimeException(
|
||||
"Cannot seek to byte {$byte} when the buffered stream only contains {$this->getSize()} bytes"
|
||||
"Cannot seek to byte {$byte} when the buffered stream only contains {$this->body->getSize()} bytes"
|
||||
);
|
||||
}
|
||||
|
||||
@ -93,16 +106,23 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
*/
|
||||
public function read($length)
|
||||
{
|
||||
$data = '';
|
||||
$remaining = $length;
|
||||
|
||||
if ($this->ftell() < $this->body->getSize()) {
|
||||
$data = $this->body->read($length);
|
||||
$remaining -= strlen($data);
|
||||
}
|
||||
// Perform a regular read on any previously read data from the buffer
|
||||
$data = $this->body->read($length);
|
||||
$remaining = $length - strlen($data);
|
||||
|
||||
// More data was requested so read from the remote stream
|
||||
if ($remaining) {
|
||||
$remoteData = $this->remoteStream->read($remaining);
|
||||
// If data was written to the buffer in a position that would have been filled from the remote stream,
|
||||
// then we must skip bytes on the remote stream to emulate overwriting bytes from that position. This
|
||||
// mimics the behavior of other PHP stream wrappers.
|
||||
$remoteData = $this->remoteStream->read($remaining + $this->skipReadBytes);
|
||||
|
||||
if ($this->skipReadBytes) {
|
||||
$len = strlen($remoteData);
|
||||
$remoteData = substr($remoteData, $this->skipReadBytes);
|
||||
$this->skipReadBytes = max(0, $this->skipReadBytes - $len);
|
||||
}
|
||||
|
||||
$data .= $remoteData;
|
||||
$this->body->write($remoteData);
|
||||
}
|
||||
@ -115,6 +135,13 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
*/
|
||||
public function write($string)
|
||||
{
|
||||
// When appending to the end of the currently read stream, you'll want to skip bytes from being read from
|
||||
// the remote stream to emulate other stream wrappers. Basically replacing bytes of data of a fixed length.
|
||||
$overflow = (strlen($string) + $this->ftell()) - $this->remoteStream->ftell();
|
||||
if ($overflow > 0) {
|
||||
$this->skipReadBytes += $overflow;
|
||||
}
|
||||
|
||||
return $this->body->write($string);
|
||||
}
|
||||
|
||||
@ -160,10 +187,6 @@ class CachingEntityBody extends AbstractEntityBodyDecorator
|
||||
public function setStream($stream, $size = 0)
|
||||
{
|
||||
$this->remoteStream->setStream($stream, $size);
|
||||
$remoteSize = $this->remoteStream->getSize();
|
||||
if (false !== $remoteSize) {
|
||||
$this->body->setSize($remoteSize);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -203,9 +203,12 @@ class Stream implements StreamInterface
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
// If the stream is a file based stream and local, then check the filesize
|
||||
if ($this->isLocal() && $this->getWrapper() == 'plainfile' && $this->getUri() && file_exists($this->getUri())) {
|
||||
return filesize($this->getUri());
|
||||
// If the stream is a file based stream and local, then use fstat
|
||||
if ($this->isLocal()) {
|
||||
$stats = fstat($this->stream);
|
||||
if (isset($stats['size'])) {
|
||||
return $stats['size'];
|
||||
}
|
||||
}
|
||||
|
||||
// Only get the size based on the content if the the stream is readable and seekable
|
||||
|
@ -26,7 +26,7 @@ class CachingEntityBodyTest extends \Guzzle\Tests\GuzzleTestCase
|
||||
$this->body = new CachingEntityBody($this->decorated);
|
||||
}
|
||||
|
||||
public function testConstructorSetsSizeUsingRemoteSize()
|
||||
public function testUsesRemoteSizeIfPossible()
|
||||
{
|
||||
$body = EntityBody::factory('test');
|
||||
$caching = new CachingEntityBody($body);
|
||||
@ -104,14 +104,12 @@ class CachingEntityBodyTest extends \Guzzle\Tests\GuzzleTestCase
|
||||
$this->body->read(2);
|
||||
$this->body->write('hi');
|
||||
$this->body->rewind();
|
||||
$this->assertEquals('tehisting', (string) $this->body);
|
||||
$this->assertEquals('tehiing', (string) $this->body);
|
||||
}
|
||||
|
||||
/**
|
||||
* @outputBufferingEnabled
|
||||
*/
|
||||
public function testReadLinesFromBothStreams()
|
||||
{
|
||||
$this->body->seek($this->body->ftell());
|
||||
$this->body->write("test\n123\nhello\n1234567890\n");
|
||||
$this->body->rewind();
|
||||
$this->assertEquals("test\n", $this->body->readLine(7));
|
||||
@ -119,8 +117,49 @@ class CachingEntityBodyTest extends \Guzzle\Tests\GuzzleTestCase
|
||||
$this->assertEquals("hello\n", $this->body->readLine(7));
|
||||
$this->assertEquals("123456", $this->body->readLine(7));
|
||||
$this->assertEquals("7890\n", $this->body->readLine(7));
|
||||
$this->assertEquals("testin", $this->body->readLine(7));
|
||||
$this->assertEquals("g", $this->body->readLine(7));
|
||||
// We overwrote the decorated stream, so no more data
|
||||
$this->assertEquals('', $this->body->readLine(7));
|
||||
}
|
||||
|
||||
public function testSkipsOverwrittenBytes()
|
||||
{
|
||||
$decorated = EntityBody::factory(
|
||||
implode("\n", array_map(function ($n) {
|
||||
return str_pad($n, 4, '0', STR_PAD_LEFT);
|
||||
}, range(0, 25)))
|
||||
);
|
||||
|
||||
$body = new CachingEntityBody($decorated);
|
||||
|
||||
$this->assertEquals("0000\n", $body->readLine());
|
||||
$this->assertEquals("0001\n", $body->readLine());
|
||||
// Write over part of the body yet to be read, so skip some bytes
|
||||
$this->assertEquals(5, $body->write("TEST\n"));
|
||||
$this->assertEquals(5, $this->readAttribute($body, 'skipReadBytes'));
|
||||
// Read, which skips bytes, then reads
|
||||
$this->assertEquals("0003\n", $body->readLine());
|
||||
$this->assertEquals(0, $this->readAttribute($body, 'skipReadBytes'));
|
||||
$this->assertEquals("0004\n", $body->readLine());
|
||||
$this->assertEquals("0005\n", $body->readLine());
|
||||
|
||||
// Overwrite part of the cached body (so don't skip any bytes)
|
||||
$body->seek(5);
|
||||
$this->assertEquals(5, $body->write("ABCD\n"));
|
||||
$this->assertEquals(0, $this->readAttribute($body, 'skipReadBytes'));
|
||||
$this->assertEquals("TEST\n", $body->readLine());
|
||||
$this->assertEquals("0003\n", $body->readLine());
|
||||
$this->assertEquals("0004\n", $body->readLine());
|
||||
$this->assertEquals("0005\n", $body->readLine());
|
||||
$this->assertEquals("0006\n", $body->readLine());
|
||||
$this->assertEquals(5, $body->write("1234\n"));
|
||||
$this->assertEquals(5, $this->readAttribute($body, 'skipReadBytes'));
|
||||
|
||||
// Seek to 0 and ensure the overwritten bit is replaced
|
||||
$body->rewind();
|
||||
$this->assertEquals("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", $body->read(50));
|
||||
|
||||
// Ensure that casting it to a string does not include the bit that was overwritten
|
||||
$this->assertContains("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", (string) $body);
|
||||
}
|
||||
|
||||
public function testWrapsContentType()
|
||||
@ -160,7 +199,7 @@ class CachingEntityBodyTest extends \Guzzle\Tests\GuzzleTestCase
|
||||
->method('getMetadata')
|
||||
->will($this->returnValue(array()));
|
||||
// Called twice for getWrapper and getWrapperData
|
||||
$a->expects($this->exactly(2))
|
||||
$a->expects($this->exactly(1))
|
||||
->method('getWrapper')
|
||||
->will($this->returnValue('wrapper'));
|
||||
$a->expects($this->once())
|
||||
|
Loading…
x
Reference in New Issue
Block a user