diff --git a/src/AdapterInterface.php b/src/AdapterInterface.php index 34dadb40..828b8050 100644 --- a/src/AdapterInterface.php +++ b/src/AdapterInterface.php @@ -184,6 +184,48 @@ public function write($fileDescriptor, $data, $length, $offset); */ public function close($fd); + /** + * Reads the entire file. + * + * This is an optimization for adapters which can optimize + * the open -> (seek ->) read -> close sequence into one call. + * + * @param string $path + * @param int $offset + * @param int|null $length + * @return PromiseInterface + */ + public function getContents($path, $offset = 0, $length = null); + + /** + * Writes the given content to the specified file. + * If the file exists, the file is truncated. + * If the file does not exist, the file will be created. + * + * This is an optimization for adapters which can optimize + * the open -> write -> close sequence into one call. + * + * @param string $path + * @param string $content + * @return PromiseInterface + * @see AdapterInterface::appendContents() + */ + public function putContents($path, $content); + + /** + * Appends the given content to the specified file. + * If the file does not exist, the file will be created. + * + * This is an optimization for adapters which can optimize + * the open -> write -> close sequence into one call. + * + * @param string $path + * @param string $content + * @return PromiseInterface + * @see AdapterInterface::putContents() + */ + public function appendContents($path, $content); + /** * Rename a node. * diff --git a/src/ChildProcess/Adapter.php b/src/ChildProcess/Adapter.php index cea8cffd..b68a3a62 100644 --- a/src/ChildProcess/Adapter.php +++ b/src/ChildProcess/Adapter.php @@ -4,6 +4,7 @@ use DateTime; use Exception; +use Throwable; use React\EventLoop\LoopInterface; use React\Filesystem\ObjectStream; use React\Filesystem\ObjectStreamSink; @@ -181,6 +182,10 @@ public function callFilesystem($function, $args, $errorResultCode = -1) return $this->pool->rpc(Factory::rpc($function, $args))->then(function (Payload $payload) { return \React\Promise\resolve($payload->getPayload()); }, function ($payload) { + if ($payload instanceof Throwable) { + return \React\Promise\reject($payload); + } + return \React\Promise\reject(new Exception($payload['error']['message'])); }); } @@ -280,7 +285,76 @@ public function close($fd) return $fileDescriptor->softTerminate(); }); } - + + /** + * Reads the entire file. + * + * This is an optimization for adapters which can optimize + * the open -> (seek ->) read -> close sequence into one call. + * + * @param string $path + * @param int $offset + * @param int|null $length + * @return PromiseInterface + */ + public function getContents($path, $offset = 0, $length = null) + { + return $this->invoker->invokeCall('getContents', [ + 'path' => $path, + 'offset' => $offset, + 'maxlen' => $length, + ])->then(function ($payload) { + return \React\Promise\resolve(base64_decode($payload['chunk'])); + }); + } + + /** + * Writes the given content to the specified file. + * If the file exists, the file is truncated. + * If the file does not exist, the file will be created. + * + * This is an optimization for adapters which can optimize + * the open -> write -> close sequence into one call. + * + * @param string $path + * @param string $content + * @return PromiseInterface + * @see AdapterInterface::appendContents() + */ + public function putContents($path, $content) + { + return $this->invoker->invokeCall('putContents', [ + 'path' => $path, + 'chunk' => base64_encode($content), + 'flags' => 0, + ])->then(function ($payload) { + return \React\Promise\resolve($payload['written']); + }); + } + + /** + * Appends the given content to the specified file. + * If the file does not exist, the file will be created. + * + * This is an optimization for adapters which can optimize + * the open -> write -> close sequence into one call. + * + * @param string $path + * @param string $content + * @return PromiseInterface + * @see AdapterInterface::putContents() + */ + public function appendContents($path, $content) + { + return $this->invoker->invokeCall('putContents', [ + 'path' => $path, + 'chunk' => base64_encode($content), + 'flags' => FILE_APPEND, + ])->then(function ($payload) { + return \React\Promise\resolve($payload['written']); + }); + } + /** * @param string $path * @return PromiseInterface diff --git a/src/ChildProcess/Process.php b/src/ChildProcess/Process.php index c3302626..63afd4ed 100644 --- a/src/ChildProcess/Process.php +++ b/src/ChildProcess/Process.php @@ -39,6 +39,8 @@ public function __construct(Messenger $messenger) 'read', 'write', 'close', + 'getContents', + 'putContents', 'rename', 'readlink', 'symlink', @@ -247,6 +249,34 @@ public function close(array $payload) ]); } + /** + * @param array $payload + * @return PromiseInterface + */ + public function getContents(array $payload) + { + if ($payload['maxlen'] > 0) { + $chunk = file_get_contents($payload['path'], false, null, $payload['offset'], $payload['maxlen']); + } else { + $chunk = file_get_contents($payload['path'], false, null, $payload['offset']); + } + + return \React\Promise\resolve([ + 'chunk' => base64_encode($chunk), + ]); + } + + /** + * @param array $payload + * @return PromiseInterface + */ + public function putContents(array $payload) + { + return \React\Promise\resolve([ + 'written' => file_put_contents($payload['path'], base64_decode($payload['chunk']), $payload['flags']), + ]); + } + /** * @param array $payload * @return PromiseInterface diff --git a/src/Eio/Adapter.php b/src/Eio/Adapter.php index 9fa82fc9..5f853307 100644 --- a/src/Eio/Adapter.php +++ b/src/Eio/Adapter.php @@ -309,6 +309,75 @@ public function close($fd) }); } + /** + * Reads the entire file. + * + * This is an optimization for adapters which can optimize + * the open -> (seek ->) read -> close sequence into one call. + * + * @param string $path + * @param int $offset + * @param int|null $length + * @return PromiseInterface + */ + public function getContents($path, $offset = 0, $length = null) + { + if ($length === null) { + return $this->stat($path)->then(function ($stat) use ($path, $offset) { + return $this->getContents($path, $offset, $stat['size']); + }); + } + + return $this->open($path, 'r')->then(function ($fd) use ($offset, $length) { + return $this->read($fd, $length, $offset)->always(function () use ($fd) { + return $this->close($fd); + }); + }); + } + + /** + * Writes the given content to the specified file. + * If the file exists, the file is truncated. + * If the file does not exist, the file will be created. + * + * This is an optimization for adapters which can optimize + * the open -> write -> close sequence into one call. + * + * @param string $path + * @param string $content + * @return PromiseInterface + * @see AdapterInterface::appendContents() + */ + public function putContents($path, $content) + { + return $this->open($path, 'cw')->then(function ($fd) use ($content) { + return $this->write($fd, $content, strlen($content), 0)->always(function () use ($fd) { + return $this->close($fd); + }); + }); + } + + /** + * Appends the given content to the specified file. + * If the file does not exist, the file will be created. + * + * This is an optimization for adapters which can optimize + * the open -> write -> close sequence into one call. + * + * @param string $path + * @param string $content + * @return PromiseInterface + * @see AdapterInterface::putContents() + */ + public function appendContents($path, $content) + { + return $this->open($path, 'cwa')->then(function ($fd) use ($content) { + return $this->write($fd, $content, strlen($content), 0)->always(function () use ($fd) { + return $this->close($fd); + }); + }); + } + /** * {@inheritDoc} */ diff --git a/tests/Adapters/AbstractAdaptersTest.php b/tests/Adapters/AbstractAdaptersTest.php index c37edcfa..d9127ea3 100644 --- a/tests/Adapters/AbstractAdaptersTest.php +++ b/tests/Adapters/AbstractAdaptersTest.php @@ -85,15 +85,6 @@ protected function getEioProvider(callable $loopFactory) ]; } - protected function getPthreadsProvider(callable $loopFactory) - { - $loop = $loopFactory(); - return [ - $loop, - new Pthreads\Adapter($loop), - ]; - } - protected function getFacoryProvider(callable $loopFactory) { $loop = $loopFactory(); diff --git a/tests/Adapters/DirectoryTest.php b/tests/Adapters/DirectoryTest.php index befb28c1..c0bc5cc3 100644 --- a/tests/Adapters/DirectoryTest.php +++ b/tests/Adapters/DirectoryTest.php @@ -3,10 +3,7 @@ namespace React\Tests\Filesystem\Adapters; use React\EventLoop\LoopInterface; -use React\Filesystem\ChildProcess; -use React\Filesystem\Eio; use React\Filesystem\FilesystemInterface; -use React\Filesystem\Pthreads; /** * @group adapters diff --git a/tests/Adapters/FileTest.php b/tests/Adapters/FileTest.php index f0e43fe0..754ee463 100644 --- a/tests/Adapters/FileTest.php +++ b/tests/Adapters/FileTest.php @@ -3,10 +3,7 @@ namespace React\Tests\Filesystem\Adapters; use React\EventLoop\LoopInterface; -use React\Filesystem\ChildProcess; -use React\Filesystem\Eio; use React\Filesystem\FilesystemInterface; -use React\Filesystem\Pthreads; /** * @group adapters diff --git a/tests/Adapters/InterfaceTest.php b/tests/Adapters/InterfaceTest.php index 70754c13..04ac3ffb 100644 --- a/tests/Adapters/InterfaceTest.php +++ b/tests/Adapters/InterfaceTest.php @@ -4,9 +4,6 @@ use React\EventLoop\LoopInterface; use React\Filesystem\AdapterInterface; -use React\Filesystem\ChildProcess; -use React\Filesystem\Eio; -use React\Filesystem\Pthreads; class InterfaceTest extends AbstractAdaptersTest { diff --git a/tests/ChildProcess/AdapterTest.php b/tests/ChildProcess/AdapterTest.php index fd7631aa..4ca0ef88 100644 --- a/tests/ChildProcess/AdapterTest.php +++ b/tests/ChildProcess/AdapterTest.php @@ -416,4 +416,50 @@ public function testErrorFromPool() ]); $this->await($adapter->touch('foo.bar'), $loop, 1); } + + public function testGetContents() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $contents = $this->await($adapter->getContents(__FILE__), $loop); + $this->assertSame(file_get_contents(__FILE__), $contents); + } + + public function testGetContentsMinMax() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $contents = $this->await($adapter->getContents(__FILE__, 5, 10), $loop); + $this->assertSame(file_get_contents(__FILE__, false, null, 5, 10), $contents); + } + + public function testPutContents() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $tempFile = $this->tmpDir . uniqid('', true); + $contents = sha1_file(__FILE__); + + $this->await($adapter->putContents($tempFile, $contents), $loop); + $this->assertSame($contents, file_get_contents($tempFile)); + } + + public function testAppendContents() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $tempFile = $this->tmpDir . uniqid('', true); + $contents = sha1_file(__FILE__); + + file_put_contents($tempFile, $contents); + $time = sha1(time()); + $contents .= $time; + + $this->await($adapter->appendContents($tempFile, $time, FILE_APPEND), $loop); + $this->assertSame($contents, file_get_contents($tempFile)); + } } diff --git a/tests/Eio/AdapterTest.php b/tests/Eio/AdapterTest.php index 998c80ff..d28aea37 100644 --- a/tests/Eio/AdapterTest.php +++ b/tests/Eio/AdapterTest.php @@ -769,4 +769,50 @@ public function testWorkPendingCount() { $this->assertInternalType('int', (new Adapter($this->getMock('React\EventLoop\LoopInterface')))->workPendingCount()); } + + public function testGetContents() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $contents = $this->await($adapter->getContents(__FILE__), $loop); + $this->assertSame(file_get_contents(__FILE__), $contents); + } + + public function testGetContentsMinMax() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $contents = $this->await($adapter->getContents(__FILE__, 5, 10), $loop); + $this->assertSame(file_get_contents(__FILE__, false, null, 5, 10), $contents); + } + + public function testPutContents() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $tempFile = $this->tmpDir . uniqid('', true); + $contents = sha1_file(__FILE__); + + $this->await($adapter->putContents($tempFile, $contents), $loop); + $this->assertSame($contents, file_get_contents($tempFile)); + } + + public function testAppendContents() + { + $loop = \React\EventLoop\Factory::create(); + $adapter = new Adapter($loop); + + $tempFile = $this->tmpDir . uniqid('', true); + $contents = sha1_file(__FILE__); + + file_put_contents($tempFile, $contents); + $time = sha1(time()); + $contents .= $time; + + $this->await($adapter->appendContents($tempFile, $time), $loop); + $this->assertSame($contents, file_get_contents($tempFile)); + } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 6d5fb585..06c75036 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -43,6 +43,9 @@ protected function mockAdapter(LoopInterface $loop = null) 'read', 'write', 'close', + 'getContents', + 'putContents', + 'appendContents', 'rename', 'readlink', 'symlink',