From 86dd2005b651c8134514d222e7b816d54977faf0 Mon Sep 17 00:00:00 2001 From: Carl Alexander Date: Fri, 14 Aug 2020 22:28:08 -0400 Subject: [PATCH] feat: add support for directories to stream wrapper --- .../CloudStorageStreamWrapper.php | 165 ++++++- .../CloudStorageStreamWrapperPhpTest.php | 117 +++-- .../CloudStorageStreamWrapperTest.php | 449 ++++++++++++++---- 3 files changed, 600 insertions(+), 131 deletions(-) diff --git a/src/CloudStorage/CloudStorageStreamWrapper.php b/src/CloudStorage/CloudStorageStreamWrapper.php index 3604dd3..95ea7f0 100644 --- a/src/CloudStorage/CloudStorageStreamWrapper.php +++ b/src/CloudStorage/CloudStorageStreamWrapper.php @@ -37,9 +37,30 @@ class CloudStorageStreamWrapper /** * Cache of object and directory lookups. * - * @var array + * @var \ArrayObject */ - private $cache = []; + private $cache; + + /** + * The cloud storage objects retrieved with "dir_opendir". + * + * @var \ArrayIterator + */ + private $openedDirectoryObjects; + + /** + * The path when "dir_opendir" was called. + * + * @var string + */ + private $openedDirectoryPath; + + /** + * The prefix used to get the cloud storage objects with "dir_opendir". + * + * @var string + */ + private $openedDirectoryPrefix; /** * Mode used when the stream was opened. @@ -65,7 +86,7 @@ class CloudStorageStreamWrapper /** * Register the cloud storage stream wrapper. */ - public static function register(CloudStorageClientInterface $client) + public static function register(CloudStorageClientInterface $client, \ArrayObject $cache = null) { if (in_array(self::PROTOCOL, stream_get_wrappers())) { stream_wrapper_unregister(self::PROTOCOL); @@ -77,9 +98,97 @@ public static function register(CloudStorageClientInterface $client) $defaultOptions[self::PROTOCOL]['client'] = $client; + if ($cache instanceof \ArrayObject) { + $defaultOptions[self::PROTOCOL]['cache'] = $cache; + } elseif (!isset($defaultOptions[self::PROTOCOL]['cache'])) { + $defaultOptions[self::PROTOCOL]['cache'] = new \ArrayObject(); + } + stream_context_set_default($defaultOptions); } + /** + * Close directory handle. + * + * @see https://www.php.net/manual/en/streamwrapper.dir-closedir.php + */ + public function dir_closedir(): bool + { + $this->openedDirectoryObjects = null; + $this->openedDirectoryPath = null; + $this->openedDirectoryPrefix = null; + gc_collect_cycles(); + + return true; + } + + /** + * Open directory handle. + * + * @see https://www.php.net/manual/en/streamwrapper.dir-opendir.php + */ + public function dir_opendir(string $path, int $options): bool + { + return $this->call(function () use ($path) { + $this->openedDirectoryPath = $path; + $this->openedDirectoryPrefix = trim($this->parsePath($path), '/').'/'; + $this->openedDirectoryObjects = new \ArrayIterator($this->getClient()->getObjects($this->openedDirectoryPrefix)); + }); + } + + /** + * Read entry from directory handle. + * + * @see https://www.php.net/manual/en/streamwrapper.dir-readdir.php + */ + public function dir_readdir() + { + if (!$this->openedDirectoryObjects instanceof \ArrayIterator || !$this->openedDirectoryObjects->valid()) { + return false; + } + + $current = $this->openedDirectoryObjects->current(); + + if (empty($current['Key'])) { + return false; + } + + $details = []; + + if (isset($current['Size'])) { + $details['size'] = $current['Size']; + } + if (isset($current['LastModified'])) { + $details['last-modified'] = $current['LastModified']; + } + + $filename = substr($current['Key'], strlen($this->openedDirectoryPrefix)); + + $this->setCacheValue($this->openedDirectoryPath.$filename, $this->getStat($current['Key'], $details)); + + $this->openedDirectoryObjects->next(); + + return $filename; + } + + /** + * Rewind directory handle. + * + * @see https://www.php.net/manual/en/streamwrapper.dir-rewinddir.php + */ + public function dir_rewinddir(): bool + { + return $this->call(function () { + if (!is_string($this->openedDirectoryPath)) { + return false; + } + + $this->dir_opendir($this->openedDirectoryPath, 0); + + return true; + }); + } + /** * Create a directory. * @@ -164,7 +273,7 @@ public function stream_cast(): bool */ public function stream_close() { - $this->cache = []; + $this->cache = null; fclose($this->openedStreamObjectResource); } @@ -196,7 +305,7 @@ public function stream_flush() $this->getClient()->putObject($this->openedStreamObjectKey, stream_get_contents($this->openedStreamObjectResource), $this->getMimetype()); - $this->removeCacheValue($this->openedStreamObjectKey); + $this->removeCacheValue(self::PROTOCOL.'://'.$this->openedStreamObjectKey); }); } @@ -353,12 +462,31 @@ private function call(callable $callback) } } + /** + * Get the cache used for storing stat values. + */ + private function getCache(): \ArrayObject + { + if (!$this->cache instanceof \ArrayObject) { + $this->cache = $this->getOption('cache') ?: new \ArrayObject(); + } + + return $this->cache; + } + /** * Get the cache value for the given key. */ private function getCacheValue(string $key) { - return $this->cache[$key] ?? null; + $cache = $this->getCache(); + $value = null; + + if ($cache->offsetExists($key)) { + $value = $cache->offsetGet($key); + } + + return $value; } /** @@ -516,9 +644,9 @@ private function getOptions(): array } /** - * Get the stat function return value with the given stat values merged in. + * Get the stat function return value with the given stat values merged in for the given object key. */ - private function getStat(string $key) + private function getStat(string $key, array $details = []) { // Default stat is directory with 0777 access $stat = [ @@ -541,15 +669,17 @@ private function getStat(string $key) return $stat; } - return $this->call(function () use ($key, $stat) { + return $this->call(function () use ($details, $key, $stat) { $client = $this->getClient(); - if (!$client->objectExists($key)) { - return false; + if (empty($details)) { + try { + $details = $client->getObjectDetails($key); + } catch (\Exception $exception) { + return false; + } } - $details = $client->getObjectDetails($key); - if ('/' === substr($key, -1) && isset($details['size']) && 0 === $details['size']) { return $stat; } @@ -607,8 +737,13 @@ private function parsePath(string $path): string */ private function removeCacheValue(string $key) { + $cache = $this->getCache(); + clearstatcache(true, $key); - unset($this->cache[$key]); + + if ($cache->offsetExists($key)) { + $cache->offsetUnset($key); + } } /** @@ -616,6 +751,6 @@ private function removeCacheValue(string $key) */ private function setCacheValue(string $key, $value) { - $this->cache[$key] = $value; + $this->getCache()->offsetSet($key, $value); } } diff --git a/tests/Integration/CloudStorage/CloudStorageStreamWrapperPhpTest.php b/tests/Integration/CloudStorage/CloudStorageStreamWrapperPhpTest.php index a8f3b09..84bce31 100644 --- a/tests/Integration/CloudStorage/CloudStorageStreamWrapperPhpTest.php +++ b/tests/Integration/CloudStorage/CloudStorageStreamWrapperPhpTest.php @@ -35,7 +35,7 @@ public function setUp() { $this->client = $this->getCloudStorageClientInterfaceMock(); - CloudStorageStreamWrapper::register($this->client); + CloudStorageStreamWrapper::register($this->client, new \ArrayObject()); } public function testAppendsToExistingFile() @@ -79,9 +79,9 @@ public function testAppendsToNonExistentFile() public function testDoesNotErrorOnFileExists() { $this->client->expects($this->once()) - ->method('objectExists') + ->method('getObjectDetails') ->with($this->identicalTo('/file')) - ->willReturn(false); + ->willThrowException(new \RuntimeException('Object "/file" not found')); $this->assertFileNotExists('cloudstorage:///file'); } @@ -89,23 +89,15 @@ public function testDoesNotErrorOnFileExists() public function testDoesNotErrorOnIsLink() { $this->client->expects($this->once()) - ->method('objectExists') + ->method('getObjectDetails') ->with($this->identicalTo('/file')) - ->willReturn(false); + ->willThrowException(new \RuntimeException('Object "/file" not found')); $this->assertFalse(is_link('cloudstorage:///file')); } public function testFileType() { - $this->client->expects($this->exactly(2)) - ->method('objectExists') - ->withConsecutive( - [$this->identicalTo('/file')], - [$this->identicalTo('/directory/')] - ) - ->willReturn(true); - $this->client->expects($this->exactly(2)) ->method('getObjectDetails') ->withConsecutive( @@ -208,9 +200,69 @@ public function testMkdirWithExistingDirectory() $this->assertFalse(mkdir('cloudstorage:///directory')); } + public function testReaddirCachesStatValue() + { + $this->client->expects($this->once()) + ->method('getObjects') + ->with($this->identicalTo('directory/')) + ->willReturn([ + ['Key' => 'directory/foo', 'Size' => 1], + ['Key' => 'directory/bar', 'Size' => 2], + ]); + + $directory = 'cloudstorage:///directory'; + $opendir = opendir($directory); + + $this->assertIsResource($opendir); + + $file1 = readdir($opendir); + $this->assertEquals('foo', $file1); + $this->assertEquals(1, filesize($directory.$file1)); + + $file2 = readdir($opendir); + $this->assertEquals('bar', $file2); + $this->assertEquals(2, filesize($directory.$file2)); + + closedir($opendir); + } + + public function testReadingDirectory() + { + $this->client->expects($this->once()) + ->method('getObjects') + ->with($this->identicalTo('directory/')) + ->willReturn([ + ['Key' => 'directory/a', 'Size' => 1], + ['Key' => 'directory/b', 'Size' => 2], + ['Key' => 'directory/c', 'Size' => 3], + ['Key' => 'directory/d', 'Size' => 4], + ['Key' => 'directory/e', 'Size' => 5], + ['Key' => 'directory/f', 'Size' => 6], + ['Key' => 'directory/g', 'Size' => 7], + ]); + + $directory = 'cloudstorage:///directory'; + $opendir = opendir($directory); + + $this->assertIsResource($opendir); + + $files = []; + while (false !== ($file = readdir($opendir))) { + $files[] = $file; + } + + $expected = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + $this->assertEquals($expected, $files); + + $this->assertSame(4, filesize($directory.'d')); + $this->assertSame(6, filesize($directory.'f')); + + closedir($opendir); + } + public function testReadingFile() { - $this->client->expects($this->exactly(2)) + $this->client->expects($this->once()) ->method('objectExists') ->with($this->identicalTo('/file')) ->willReturn(true); @@ -294,7 +346,7 @@ public function testRenameWithDifferentProtocols() public function testReturnsStreamSizeFromHeaders() { - $this->client->expects($this->exactly(2)) + $this->client->expects($this->once()) ->method('objectExists') ->with($this->identicalTo('/file')) ->willReturn(true); @@ -370,6 +422,19 @@ public function testRmdirWithNothing() $this->assertFalse(rmdir('cloudstorage://')); } + public function testScandirWithRegularDirectory() + { + $this->client->expects($this->once()) + ->method('getObjects') + ->with($this->identicalTo('directory/')) + ->willReturn([ + ['Key' => 'directory/foo'], + ['Key' => 'directory/bar'], + ]); + + $this->assertSame(['bar', 'foo'], scandir('cloudstorage:///directory')); + } + public function testStatWithProtocol() { clearstatcache(false, 'cloudstorage://'); @@ -433,11 +498,6 @@ public function testUnlinkWhenDeleteObjectThrowsException() public function testUrlStatDataClearedOnWrite() { - $this->client->expects($this->exactly(2)) - ->method('objectExists') - ->with($this->identicalTo('/file')) - ->willReturn(true); - $this->client->expects($this->exactly(2)) ->method('getObjectDetails') ->with($this->identicalTo('/file')) @@ -461,11 +521,6 @@ public function testUrlStatReturnsObjectDetails() { $time = strtotime('now'); - $this->client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/file')) - ->willReturn(true); - $this->client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/file')) @@ -482,11 +537,6 @@ public function testUrlStatReturnsObjectDetails() public function testUrlStatUsesCacheData() { - $this->client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/file')) - ->willReturn(true); - $this->client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/file')) @@ -499,12 +549,7 @@ public function testUrlStatUsesCacheData() public function testUrlStatWhenGetObjectDetailsThrowsException() { $this->expectException(Warning::class); - $this->expectExceptionMessage('Object "/file" not found'); - - $this->client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/file')) - ->willReturn(true); + $this->expectExceptionMessage('filesize(): stat failed for cloudstorage:///file'); $this->client->expects($this->once()) ->method('getObjectDetails') diff --git a/tests/Unit/CloudStorage/CloudStorageStreamWrapperTest.php b/tests/Unit/CloudStorage/CloudStorageStreamWrapperTest.php index bef67b2..e951542 100644 --- a/tests/Unit/CloudStorage/CloudStorageStreamWrapperTest.php +++ b/tests/Unit/CloudStorage/CloudStorageStreamWrapperTest.php @@ -27,6 +27,333 @@ class CloudStorageStreamWrapperTest extends TestCase use CloudStorageClientInterfaceMockTrait; use FunctionMockTrait; + public function testDirClosedir() + { + $wrapper = new CloudStorageStreamWrapper(); + $wrapperReflection = new \ReflectionObject($wrapper); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, new \ArrayIterator()); + + $openedDirectoryPathReflection = $wrapperReflection->getProperty('openedDirectoryPath'); + $openedDirectoryPathReflection->setAccessible(true); + $openedDirectoryPathReflection->setValue($wrapper, 'cloudstorage:///directory/'); + + $openedDirectoryPrefixReflection = $wrapperReflection->getProperty('openedDirectoryPrefix'); + $openedDirectoryPrefixReflection->setAccessible(true); + $openedDirectoryPrefixReflection->setValue($wrapper, 'directory/'); + + $gc_collect_cycles = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'gc_collect_cycles'); + $gc_collect_cycles->expects($this->once()); + + $wrapper->dir_closedir(); + + $this->assertNull($openedDirectoryObjectsReflection->getValue($wrapper)); + $this->assertNull($openedDirectoryPathReflection->getValue($wrapper)); + $this->assertNull($openedDirectoryPrefixReflection->getValue($wrapper)); + } + + public function testDirOpendir() + { + $client = $this->getCloudStorageClientInterfaceMock(); + $objects = [ + ['Key' => 'directory/foo'], + ['Key' => 'directory/bar'], + ]; + $wrapper = new CloudStorageStreamWrapper(); + $wrapperReflection = new \ReflectionObject($wrapper); + + $client->expects($this->once()) + ->method('getObjects') + ->with($this->identicalTo('directory/')) + ->willReturn($objects); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + + $openedDirectoryPathReflection = $wrapperReflection->getProperty('openedDirectoryPath'); + $openedDirectoryPathReflection->setAccessible(true); + + $openedDirectoryPrefixReflection = $wrapperReflection->getProperty('openedDirectoryPrefix'); + $openedDirectoryPrefixReflection->setAccessible(true); + + CloudStorageStreamWrapper::register($client, new \ArrayObject()); + + $this->assertTrue($wrapper->dir_opendir('cloudstorage:///directory', 0)); + + $openedDirectoryObjects = $openedDirectoryObjectsReflection->getValue($wrapper); + + $this->assertInstanceOf(\ArrayIterator::class, $openedDirectoryObjects); + $this->assertSame($objects, $openedDirectoryObjects->getArrayCopy()); + $this->assertSame('cloudstorage:///directory', $openedDirectoryPathReflection->getValue($wrapper)); + $this->assertSame('directory/', $openedDirectoryPrefixReflection->getValue($wrapper)); + } + + public function testDirReaddirWhenOpenedDirectoryObjectIsInvalid() + { + $objects = $this->getMockBuilder(\ArrayIterator::class)->getMock(); + $wrapper = new CloudStorageStreamWrapper(); + + $objects->expects($this->once()) + ->method('valid') + ->willReturn(false); + + $wrapperReflection = new \ReflectionObject($wrapper); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, $objects); + + $this->assertFalse($wrapper->dir_readdir()); + } + + public function testDirReaddirWhenOpenedDirectoryObjectIsNull() + { + $wrapper = new CloudStorageStreamWrapper(); + + $this->assertFalse($wrapper->dir_readdir()); + } + + public function testDirReaddirWhenOpenedDirectoryObjectReturnsObjectWithNoKey() + { + $objects = $this->getMockBuilder(\ArrayIterator::class)->getMock(); + $wrapper = new CloudStorageStreamWrapper(); + + $objects->expects($this->once()) + ->method('valid') + ->willReturn(true); + + $objects->expects($this->once()) + ->method('current') + ->willReturn([]); + + $wrapperReflection = new \ReflectionObject($wrapper); + + $cacheReflection = $wrapperReflection->getProperty('cache'); + $cacheReflection->setAccessible(true); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, $objects); + + $this->assertFalse($wrapper->dir_readdir()); + } + + public function testDirReaddirWithLastModified() + { + $objects = $this->getMockBuilder(\ArrayIterator::class)->getMock(); + $client = $this->getCloudStorageClientInterfaceMock(); + $wrapper = new CloudStorageStreamWrapper(); + + $objects->expects($this->once()) + ->method('valid') + ->willReturn(true); + + $objects->expects($this->once()) + ->method('current') + ->willReturn(['Key' => 'directory/file', 'LastModified' => '10 September 2000']); + + $objects->expects($this->once()) + ->method('next'); + + $wrapperReflection = new \ReflectionObject($wrapper); + + $cacheReflection = $wrapperReflection->getProperty('cache'); + $cacheReflection->setAccessible(true); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, $objects); + + $openedDirectoryPathReflection = $wrapperReflection->getProperty('openedDirectoryPath'); + $openedDirectoryPathReflection->setAccessible(true); + $openedDirectoryPathReflection->setValue($wrapper, 'cloudstorage:///directory/'); + + $openedDirectoryPrefixReflection = $wrapperReflection->getProperty('openedDirectoryPrefix'); + $openedDirectoryPrefixReflection->setAccessible(true); + $openedDirectoryPrefixReflection->setValue($wrapper, 'directory/'); + + CloudStorageStreamWrapper::register($client, new \ArrayObject()); + + $expectedStat = [ + 0 => 0, 'dev' => 0, + 1 => 0, 'ino' => 0, + 2 => 0100777, 'mode' => 0100777, + 3 => 0, 'nlink' => 0, + 4 => 0, 'uid' => 0, + 5 => 0, 'gid' => 0, + 6 => -1, 'rdev' => -1, + 7 => 0, 'size' => 0, + 8 => 0, 'atime' => 0, + 9 => 968544000, 'mtime' => 968544000, + 10 => 968544000, 'ctime' => 968544000, + 11 => -1, 'blksize' => -1, + 12 => -1, 'blocks' => -1, + ]; + + $this->assertSame('file', $wrapper->dir_readdir()); + $this->assertSame(['cloudstorage:///directory/file' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); + } + + public function testDirReaddirWithNoLastModifiedOrSize() + { + $objects = $this->getMockBuilder(\ArrayIterator::class)->getMock(); + $client = $this->getCloudStorageClientInterfaceMock(); + $wrapper = new CloudStorageStreamWrapper(); + + $objects->expects($this->once()) + ->method('valid') + ->willReturn(true); + + $objects->expects($this->once()) + ->method('current') + ->willReturn(['Key' => 'directory/file']); + + $objects->expects($this->once()) + ->method('next'); + + $wrapperReflection = new \ReflectionObject($wrapper); + + $cacheReflection = $wrapperReflection->getProperty('cache'); + $cacheReflection->setAccessible(true); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, $objects); + + $openedDirectoryPathReflection = $wrapperReflection->getProperty('openedDirectoryPath'); + $openedDirectoryPathReflection->setAccessible(true); + $openedDirectoryPathReflection->setValue($wrapper, 'cloudstorage:///directory/'); + + $openedDirectoryPrefixReflection = $wrapperReflection->getProperty('openedDirectoryPrefix'); + $openedDirectoryPrefixReflection->setAccessible(true); + $openedDirectoryPrefixReflection->setValue($wrapper, 'directory/'); + + CloudStorageStreamWrapper::register($client, new \ArrayObject()); + + $expectedStat = [ + 0 => 0, 'dev' => 0, + 1 => 0, 'ino' => 0, + 2 => 0100777, 'mode' => 0100777, + 3 => 0, 'nlink' => 0, + 4 => 0, 'uid' => 0, + 5 => 0, 'gid' => 0, + 6 => -1, 'rdev' => -1, + 7 => 0, 'size' => 0, + 8 => 0, 'atime' => 0, + 9 => 0, 'mtime' => 0, + 10 => 0, 'ctime' => 0, + 11 => -1, 'blksize' => -1, + 12 => -1, 'blocks' => -1, + ]; + + $this->assertSame('file', $wrapper->dir_readdir()); + $this->assertSame(['cloudstorage:///directory/file' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); + } + + public function testDirReaddirWithSize() + { + $objects = $this->getMockBuilder(\ArrayIterator::class)->getMock(); + $client = $this->getCloudStorageClientInterfaceMock(); + $wrapper = new CloudStorageStreamWrapper(); + + $objects->expects($this->once()) + ->method('valid') + ->willReturn(true); + + $objects->expects($this->once()) + ->method('current') + ->willReturn(['Key' => 'directory/file', 'Size' => 42]); + + $objects->expects($this->once()) + ->method('next'); + + $wrapperReflection = new \ReflectionObject($wrapper); + + $cacheReflection = $wrapperReflection->getProperty('cache'); + $cacheReflection->setAccessible(true); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, $objects); + + $openedDirectoryPathReflection = $wrapperReflection->getProperty('openedDirectoryPath'); + $openedDirectoryPathReflection->setAccessible(true); + $openedDirectoryPathReflection->setValue($wrapper, 'cloudstorage:///directory/'); + + $openedDirectoryPrefixReflection = $wrapperReflection->getProperty('openedDirectoryPrefix'); + $openedDirectoryPrefixReflection->setAccessible(true); + $openedDirectoryPrefixReflection->setValue($wrapper, 'directory/'); + + CloudStorageStreamWrapper::register($client, new \ArrayObject()); + + $expectedStat = [ + 0 => 0, 'dev' => 0, + 1 => 0, 'ino' => 0, + 2 => 0100777, 'mode' => 0100777, + 3 => 0, 'nlink' => 0, + 4 => 0, 'uid' => 0, + 5 => 0, 'gid' => 0, + 6 => -1, 'rdev' => -1, + 7 => 42, 'size' => 42, + 8 => 0, 'atime' => 0, + 9 => 0, 'mtime' => 0, + 10 => 0, 'ctime' => 0, + 11 => -1, 'blksize' => -1, + 12 => -1, 'blocks' => -1, + ]; + + $this->assertSame('file', $wrapper->dir_readdir()); + $this->assertSame(['cloudstorage:///directory/file' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); + } + + public function testDirRewinddirWithInvalidopenedDirectoryPrefix() + { + $wrapper = new CloudStorageStreamWrapper(); + + $this->assertFalse($wrapper->dir_rewinddir()); + } + + public function testDirRewinddirWithValidopenedDirectoryPrefix() + { + $client = $this->getCloudStorageClientInterfaceMock(); + $objects = [ + ['Key' => 'directory/foo'], + ['Key' => 'directory/bar'], + ]; + $wrapper = new CloudStorageStreamWrapper(); + $wrapperReflection = new \ReflectionObject($wrapper); + + $client->expects($this->once()) + ->method('getObjects') + ->with($this->identicalTo('directory/')) + ->willReturn($objects); + + $openedDirectoryObjectsReflection = $wrapperReflection->getProperty('openedDirectoryObjects'); + $openedDirectoryObjectsReflection->setAccessible(true); + $openedDirectoryObjectsReflection->setValue($wrapper, new \ArrayIterator()); + + $openedDirectoryPathReflection = $wrapperReflection->getProperty('openedDirectoryPath'); + $openedDirectoryPathReflection->setAccessible(true); + $openedDirectoryPathReflection->setValue($wrapper, 'cloudstorage:///directory/'); + + $openedDirectoryPrefixReflection = $wrapperReflection->getProperty('openedDirectoryPrefix'); + $openedDirectoryPrefixReflection->setAccessible(true); + $openedDirectoryPrefixReflection->setValue($wrapper, 'directory/'); + + CloudStorageStreamWrapper::register($client, new \ArrayObject()); + + $this->assertTrue($wrapper->dir_rewinddir()); + + $openedDirectoryObjects = $openedDirectoryObjectsReflection->getValue($wrapper); + + $this->assertInstanceOf(\ArrayIterator::class, $openedDirectoryObjects); + $this->assertSame($objects, $openedDirectoryObjects->getArrayCopy()); + $this->assertSame('cloudstorage:///directory/', $openedDirectoryPathReflection->getValue($wrapper)); + $this->assertSame('directory/', $openedDirectoryPrefixReflection->getValue($wrapper)); + } + public function testMkdirWhenDirectoryDoesntExist() { $client = $this->getCloudStorageClientInterfaceMock(); @@ -44,7 +371,7 @@ public function testMkdirWhenDirectoryDoesntExist() $clearstatcache->expects($this->once()) ->with($this->identicalTo(true), $this->identicalTo('cloudstorage:///foo')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->mkdir('cloudstorage:///foo', 0777); } @@ -61,7 +388,7 @@ public function testMkdirWhenDirectoryExists() ->with($this->identicalTo('/foo/')) ->willReturn(true); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->mkdir('cloudstorage:///foo', 0777); } @@ -71,6 +398,7 @@ public function testMkdirWhenDirectoryExists() */ public function testRegisterWithExistingWrapper() { + $cache = new \ArrayObject(); $client = $this->getCloudStorageClientInterfaceMock(); $stream_context_get_options = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_context_get_options'); @@ -79,7 +407,7 @@ public function testRegisterWithExistingWrapper() $stream_context_set_default = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_context_set_default'); $stream_context_set_default->expects($this->once()) - ->with($this->identicalTo(['cloudstorage' => ['client' => $client]])); + ->with($this->identicalTo(['cloudstorage' => ['client' => $client, 'cache' => $cache]])); $stream_get_wrappers = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_get_wrappers'); $stream_get_wrappers->expects($this->once()) @@ -93,7 +421,7 @@ public function testRegisterWithExistingWrapper() $stream_wrapper_unregister->expects($this->once()) ->with($this->identicalTo('cloudstorage')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, $cache); } /** @@ -101,6 +429,7 @@ public function testRegisterWithExistingWrapper() */ public function testRegisterWithoutExistingWrapper() { + $cache = new \ArrayObject(); $client = $this->getCloudStorageClientInterfaceMock(); $stream_context_get_options = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_context_get_options'); @@ -109,7 +438,7 @@ public function testRegisterWithoutExistingWrapper() $stream_context_set_default = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_context_set_default'); $stream_context_set_default->expects($this->once()) - ->with($this->identicalTo(['cloudstorage' => ['client' => $client]])); + ->with($this->identicalTo(['cloudstorage' => ['client' => $client, 'cache' => $cache]])); $stream_get_wrappers = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_get_wrappers'); $stream_get_wrappers->expects($this->once()) @@ -122,7 +451,7 @@ public function testRegisterWithoutExistingWrapper() $stream_wrapper_unregister = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'stream_wrapper_unregister'); $stream_wrapper_unregister->expects($this->never()); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, $cache); } public function testRenameSuccessful() @@ -144,7 +473,7 @@ public function testRenameSuccessful() [$this->identicalTo(true), $this->identicalTo('cloudstorage:///bar.txt')] ); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->rename('cloudstorage:///foo.txt', 'cloudstorage:///bar.txt'); } @@ -166,7 +495,7 @@ public function testRmdirWithEmptydirectory() $clearstatcache->expects($this->once()) ->with($this->identicalTo(true), $this->identicalTo('cloudstorage:///foo')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->rmdir('cloudstorage:///foo', 0777); } @@ -190,7 +519,7 @@ public function testRmdirWithNonEmptydirectory() $clearstatcache->expects($this->once()) ->with($this->identicalTo(true), $this->identicalTo('cloudstorage:///foo')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->rmdir('cloudstorage:///foo', 0777); } @@ -216,7 +545,7 @@ public function testStreamClose() $wrapper->stream_close(); - $this->assertSame([], $cacheReflection->getValue($wrapper)); + $this->assertNull($cacheReflection->getValue($wrapper)); } public function testStreamEof() @@ -255,7 +584,7 @@ public function testStreamFlushWhenNotReading() ->method('putObject') ->with($this->identicalTo('/foo.txt'), $this->identicalTo('foo'), $this->identicalTo('text/plain')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertTrue($wrapper->stream_flush()); } @@ -304,7 +633,7 @@ public function testStreamOpenWithModeA() $fwrite->expects($this->once()) ->with($this->callback(function ($value) { return is_resource($value); }), $this->identicalTo('foo')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->stream_open('cloudstorage:///foo.txt', 'a'); } @@ -324,7 +653,7 @@ public function testStreamOpenWithModeRAndFileDoesntExist() $fwrite = $this->getFunctionMock($this->getNamespace(CloudStorageStreamWrapper::class), 'fwrite'); $fwrite->expects($this->never()); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->stream_open('cloudstorage:///foo.txt', 'r'); } @@ -351,7 +680,7 @@ public function testStreamOpenWithModeRAndFileExists() $rewind->expects($this->once()) ->with($this->callback(function ($value) { return is_resource($value); })); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->stream_open('cloudstorage:///foo.txt', 'r'); } @@ -368,7 +697,7 @@ public function testStreamOpenWithModeW() $fwrite->expects($this->once()) ->with($this->callback(function ($value) { return is_resource($value); }), $this->identicalTo('')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->stream_open('cloudstorage:///foo.txt', 'w'); } @@ -390,7 +719,7 @@ public function testStreamOpenWithModeXAndFileDoesntExist() $fwrite->expects($this->once()) ->with($this->callback(function ($value) { return is_resource($value); }), $this->identicalTo('')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->stream_open('cloudstorage:///foo.txt', 'x'); } @@ -407,7 +736,7 @@ public function testStreamOpenWithModeXAndFileExists() ->with($this->identicalTo('/foo.txt')) ->willReturn(true); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); (new CloudStorageStreamWrapper())->stream_open('cloudstorage:///foo.txt', 'x'); } @@ -438,9 +767,9 @@ public function testStreamStatWhenObjectDoesntExist() $wrapper = new CloudStorageStreamWrapper(); $client->expects($this->once()) - ->method('objectExists') + ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) - ->willReturn(false); + ->willThrowException(new \RuntimeException('Object "/foo.txt" not found')); $wrapperReflection = new \ReflectionObject($wrapper); @@ -448,7 +777,7 @@ public function testStreamStatWhenObjectDoesntExist() $keyReflection->setAccessible(true); $keyReflection->setValue($wrapper, '/foo.txt'); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertFalse($wrapper->stream_stat()); } @@ -458,11 +787,6 @@ public function testStreamStatWithDirectory() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/directory/')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/directory/')) @@ -474,7 +798,7 @@ public function testStreamStatWithDirectory() $keyReflection->setAccessible(true); $keyReflection->setValue($wrapper, '/directory/'); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertSame([ 0 => 0, 'dev' => 0, @@ -498,11 +822,6 @@ public function testStreamStatWithFileSize() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/foo.txt')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) @@ -514,7 +833,7 @@ public function testStreamStatWithFileSize() $keyReflection->setAccessible(true); $keyReflection->setValue($wrapper, '/foo.txt'); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertSame([ 0 => 0, 'dev' => 0, @@ -538,11 +857,6 @@ public function testStreamStatWithLastModified() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/foo.txt')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) @@ -554,7 +868,7 @@ public function testStreamStatWithLastModified() $keyReflection->setAccessible(true); $keyReflection->setValue($wrapper, '/foo.txt'); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertSame([ 0 => 0, 'dev' => 0, @@ -578,11 +892,6 @@ public function testStreamStatWithRegularFile() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/foo.txt')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) @@ -594,7 +903,7 @@ public function testStreamStatWithRegularFile() $keyReflection->setAccessible(true); $keyReflection->setValue($wrapper, '/foo.txt'); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertSame([ 0 => 0, 'dev' => 0, @@ -645,7 +954,7 @@ public function testUnlink() $clearstatcache->expects($this->once()) ->with($this->identicalTo(true), $this->identicalTo('cloudstorage:///foo.txt')); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertTrue((new CloudStorageStreamWrapper())->unlink('cloudstorage:///foo.txt')); } @@ -659,11 +968,11 @@ public function testUrlStatWhenCached() $cacheReflection = $wrapperReflection->getProperty('cache'); $cacheReflection->setAccessible(true); - $cacheReflection->setValue($wrapper, ['cloudstorage:///foo.txt' => ['foo']]); + $cacheReflection->setValue($wrapper, new \ArrayObject(['cloudstorage:///foo.txt' => ['foo_stat']])); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); - $this->assertSame(['foo'], $wrapper->url_stat('cloudstorage:///foo.txt', 1)); + $this->assertSame(['foo_stat'], $wrapper->url_stat('cloudstorage:///foo.txt', 1)); } public function testUrlStatWhenObjectDoesntExist() @@ -672,19 +981,19 @@ public function testUrlStatWhenObjectDoesntExist() $wrapper = new CloudStorageStreamWrapper(); $client->expects($this->once()) - ->method('objectExists') + ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) - ->willReturn(false); + ->willThrowException(new \RuntimeException('Object "/foo.txt" not found')); $wrapperReflection = new \ReflectionObject($wrapper); $cacheReflection = $wrapperReflection->getProperty('cache'); $cacheReflection->setAccessible(true); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertFalse($wrapper->url_stat('cloudstorage:///foo.txt', 1)); - $this->assertSame(['cloudstorage:///foo.txt' => false], $cacheReflection->getValue($wrapper)); + $this->assertSame(['cloudstorage:///foo.txt' => false], $cacheReflection->getValue($wrapper)->getArrayCopy()); } public function testUrlStatWithDirectory() @@ -692,11 +1001,6 @@ public function testUrlStatWithDirectory() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/directory/')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/directory/')) @@ -723,10 +1027,10 @@ public function testUrlStatWithDirectory() 12 => -1, 'blocks' => -1, ]; - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $this->assertSame($expectedStat, $wrapper->url_stat('cloudstorage:///directory/', 1)); - $this->assertSame(['cloudstorage:///directory/' => $expectedStat], $cacheReflection->getValue($wrapper)); + $this->assertSame(['cloudstorage:///directory/' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); } public function testUrlStatWithFileSize() @@ -734,11 +1038,6 @@ public function testUrlStatWithFileSize() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/foo.txt')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) @@ -749,7 +1048,7 @@ public function testUrlStatWithFileSize() $cacheReflection = $wrapperReflection->getProperty('cache'); $cacheReflection->setAccessible(true); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $expectedStat = [ 0 => 0, 'dev' => 0, @@ -768,7 +1067,7 @@ public function testUrlStatWithFileSize() ]; $this->assertSame($expectedStat, $wrapper->url_stat('cloudstorage:///foo.txt', 1)); - $this->assertSame(['cloudstorage:///foo.txt' => $expectedStat], $cacheReflection->getValue($wrapper)); + $this->assertSame(['cloudstorage:///foo.txt' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); } public function testUrlStatWithLastModified() @@ -776,11 +1075,6 @@ public function testUrlStatWithLastModified() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/foo.txt')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) @@ -791,7 +1085,7 @@ public function testUrlStatWithLastModified() $cacheReflection = $wrapperReflection->getProperty('cache'); $cacheReflection->setAccessible(true); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $expectedStat = [ 0 => 0, 'dev' => 0, @@ -810,7 +1104,7 @@ public function testUrlStatWithLastModified() ]; $this->assertSame($expectedStat, $wrapper->url_stat('cloudstorage:///foo.txt', 1)); - $this->assertSame(['cloudstorage:///foo.txt' => $expectedStat], $cacheReflection->getValue($wrapper)); + $this->assertSame(['cloudstorage:///foo.txt' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); } public function testUrlStatWithRegularFile() @@ -818,11 +1112,6 @@ public function testUrlStatWithRegularFile() $client = $this->getCloudStorageClientInterfaceMock(); $wrapper = new CloudStorageStreamWrapper(); - $client->expects($this->once()) - ->method('objectExists') - ->with($this->identicalTo('/foo.txt')) - ->willReturn(true); - $client->expects($this->once()) ->method('getObjectDetails') ->with($this->identicalTo('/foo.txt')) @@ -833,7 +1122,7 @@ public function testUrlStatWithRegularFile() $cacheReflection = $wrapperReflection->getProperty('cache'); $cacheReflection->setAccessible(true); - CloudStorageStreamWrapper::register($client); + CloudStorageStreamWrapper::register($client, new \ArrayObject()); $expectedStat = [ 0 => 0, 'dev' => 0, @@ -852,6 +1141,6 @@ public function testUrlStatWithRegularFile() ]; $this->assertSame($expectedStat, $wrapper->url_stat('cloudstorage:///foo.txt', 1)); - $this->assertSame(['cloudstorage:///foo.txt' => $expectedStat], $cacheReflection->getValue($wrapper)); + $this->assertSame(['cloudstorage:///foo.txt' => $expectedStat], $cacheReflection->getValue($wrapper)->getArrayCopy()); } }