1
0
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:
Michael Dowling 2013-04-02 22:36:51 -07:00
parent 1e0a4c1b3e
commit cfd7f672e8
4 changed files with 99 additions and 34 deletions

View File

@ -92,7 +92,7 @@ class AbstractEntityBodyDecorator implements EntityBodyInterface
*/
public function getContentLength()
{
return $this->body->getContentLength();
return $this->getSize();
}
/**

View File

@ -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);
}
}
/**

View File

@ -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

View File

@ -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())