From 6ada1f4d88dae02713bc00bed28ca2d75e0586e4 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 22 Dec 2022 16:01:24 +0300 Subject: [PATCH 1/6] Implement compatibility with league/flysystem:1.x.x --- .gitignore | 1 + composer.json | 12 +- phpunit.xml | 21 +- src/GoogleStorageAdapter.php | 343 +++++++-- tests/GoogleStorageAdapterTests.php | 1040 +++++++++++++++------------ 5 files changed, 893 insertions(+), 524 deletions(-) diff --git a/.gitignore b/.gitignore index ff2e469..43c4eb0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ bin coverage coverage.xml dev +/.phpunit.result.cache diff --git a/composer.json b/composer.json index 3972104..082ba73 100755 --- a/composer.json +++ b/composer.json @@ -9,19 +9,23 @@ } ], "require": { - "php": ">=5.5.0", - "league/flysystem": "~1.0", + "php": "^7.4", + "league/flysystem": "^2.0", "google/cloud-storage": "~1.0" }, "require-dev": { - "phpunit/phpunit": "~4.0", - "mockery/mockery": "0.9.*" + "phpunit/phpunit": "^9.5" }, "autoload": { "psr-4": { "Superbalist\\Flysystem\\GoogleStorage\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Superbalist\\Flysystem\\GoogleStorage\\Test\\": "tests/" + } + }, "config": { "bin-dir": "bin" }, diff --git a/phpunit.xml b/phpunit.xml index d870901..7d8e09d 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,25 +8,26 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="true" verbose="true" > + ./tests/ - - - ./src/ - - - - - + + + + src + + + + + diff --git a/src/GoogleStorageAdapter.php b/src/GoogleStorageAdapter.php index d5e6c9d..e228fc0 100755 --- a/src/GoogleStorageAdapter.php +++ b/src/GoogleStorageAdapter.php @@ -8,12 +8,13 @@ use Google\Cloud\Storage\StorageClient; use Google\Cloud\Storage\StorageObject; use GuzzleHttp\Psr7\StreamWrapper; -use League\Flysystem\Adapter\AbstractAdapter; -use League\Flysystem\AdapterInterface; use League\Flysystem\Config; -use League\Flysystem\Util; +use League\Flysystem\FileAttributes; +use League\Flysystem\FilesystemAdapter; +use League\Flysystem\UnableToCopyFile; +use League\Flysystem\Visibility; -class GoogleStorageAdapter extends AbstractAdapter +class GoogleStorageAdapter implements FilesystemAdapter { /** * @const STORAGE_API_URI_DEFAULT @@ -30,6 +31,16 @@ class GoogleStorageAdapter extends AbstractAdapter */ protected $bucket; + /** + * @var string|null path prefix + */ + protected ?string $pathPrefix = null; + + /** + * @var string + */ + protected string $pathSeparator = '/'; + /** * @var string */ @@ -53,6 +64,72 @@ public function __construct(StorageClient $storageClient, Bucket $bucket, $pathP $this->storageApiUri = ($storageApiUri) ?: self::STORAGE_API_URI_DEFAULT; } + /** + * Prefix a path. + * + * The method grabbed from class \League\Flysystem\Adapter\AbstractAdapter of league/flysystem:dev-1.0.x. + * It is public for the backward compatibility only. + */ + public function applyPathPrefix(string $path): string + { + return $this->getPathPrefix() . ltrim($path, '\\/'); + } + + public function fileExists(string $path): bool + { + return $this->getObject($path)->exists(); + } + + public function fileSize(string $path): FileAttributes + { + $metadata = $this->getMetadata($path); + + return new FileAttributes($metadata['path'], $metadata['size']); + } + + public function getFileAttributes(string $path): FileAttributes + { + $metadata = $this->getMetadata($path); + + return new FileAttributes( + $metadata['path'], + $metadata['size'], + $this->getRawVisibility($path), + $metadata['timestamp'], + $metadata['mimetype'], + [ + 'dirname' => $metadata['dirname'], + 'type' => $metadata['type'], + ], + ); + } + + /** + * The method grabbed from class \League\Flysystem\Adapter\AbstractAdapter of league/flysystem:dev-1.0.x. + * It is public for the backward compatibility only. + */ + public function getPathPrefix(): ?string + { + return $this->pathPrefix; + } + + /** + * The method grabbed from class \League\Flysystem\Adapter\AbstractAdapter of league/flysystem:dev-1.0.x. + * It is public for the backward compatibility only. + */ + public function setPathPrefix(?string $prefix): void + { + $prefix = (string) $prefix; + + if ($prefix === '') { + $this->pathPrefix = null; + + return; + } + + $this->pathPrefix = rtrim($prefix, '\\/') . $this->pathSeparator; + } + /** * Returns the StorageClient. * @@ -93,20 +170,48 @@ public function getStorageApiUri() return $this->storageApiUri; } + public function lastModified(string $path): FileAttributes + { + $metadata = $this->getMetadata($path); + + return new FileAttributes($metadata['path'], null, null, $metadata['timestamp']); + } + + public function mimeType(string $path): FileAttributes + { + $metadata = $this->getMetadata($path); + + return new FileAttributes($metadata['path'], null, null, null, $metadata['mimetype']); + } + + /** + * The method grabbed from class \League\Flysystem\Adapter\AbstractAdapter of league/flysystem:dev-1.0.x. + * It is public for the backward compatibility only. + */ + public function removePathPrefix(string $path): string + { + return substr($path, strlen($this->getPathPrefix())); + } + + public function visibility(string $path): FileAttributes + { + return new FileAttributes($path, null, $this->getRawVisibility($path)); + } + /** * {@inheritdoc} */ - public function write($path, $contents, Config $config) + public function write(string $path, string $contents, Config $config): void { - return $this->upload($path, $contents, $config); + $this->upload($path, $contents, $config); } /** * {@inheritdoc} */ - public function writeStream($path, $resource, Config $config) + public function writeStream(string $path, $resource, Config $config): void { - return $this->upload($path, $resource, $config); + $this->upload($path, $resource, $config); } /** @@ -145,7 +250,7 @@ protected function getOptionsFromConfig(Config $config) } else { // if a file is created without an acl, it isn't accessible via the console // we therefore default to private - $options['predefinedAcl'] = $this->getPredefinedAclForVisibility(AdapterInterface::VISIBILITY_PRIVATE); + $options['predefinedAcl'] = $this->getPredefinedAclForVisibility(Visibility::PRIVATE); } if ($metadata = $config->get('metadata')) { @@ -195,7 +300,7 @@ protected function normaliseObject(StorageObject $object) return [ 'type' => $isDir ? 'dir' : 'file', - 'dirname' => Util::dirname($name), + 'dirname' => $this->dirname($name), 'path' => $name, 'timestamp' => strtotime($info['updated']), 'mimetype' => isset($info['contentType']) ? $info['contentType'] : '', @@ -206,19 +311,19 @@ protected function normaliseObject(StorageObject $object) /** * {@inheritdoc} */ - public function rename($path, $newpath) + public function move(string $source, string $destination, Config $config): void { - if (!$this->copy($path, $newpath)) { - return false; + try { + $this->copy($source, $destination, $config); + } catch (UnableToCopyFile $exception){ + $this->delete($source); } - - return $this->delete($path); } /** * {@inheritdoc} */ - public function copy($path, $newpath) + public function copy(string $path, string $newpath, Config $config): void { $newpath = $this->applyPathPrefix($newpath); @@ -229,25 +334,30 @@ public function copy($path, $newpath) 'name' => $newpath, 'predefinedAcl' => $this->getPredefinedAclForVisibility($visibility), ]; - $this->getObject($path)->copy($this->bucket, $options); - - return true; + if (!$this->getObject($path)->copy($this->bucket, $options)->exists()) { + throw UnableToCopyFile::fromLocationTo($path, $newpath); + } } /** * {@inheritdoc} */ - public function delete($path) + public function delete(string $path): void { $this->getObject($path)->delete(); - - return true; } /** - * {@inheritdoc} + * @deprecated Use {@see self::deleteDirectory() } */ public function deleteDir($dirname) + { + @trigger_error(sprintf('Method "%s:deleteDir()" id deprecated. Use "%1$s:deleteDirectory()"', __CLASS__), \E_USER_DEPRECATED); + + $this->deleteDirectory($dirname); + } + + public function deleteDirectory(string $dirname): void { $dirname = $this->normaliseDirName($dirname); $objects = $this->listContents($dirname, true); @@ -275,18 +385,26 @@ public function deleteDir($dirname) foreach ($filtered_objects as $object) { $this->delete($object['path']); } - - return true; } /** - * {@inheritdoc} + * @deprecated Use {@see self::createDirectory() } */ public function createDir($dirname, Config $config) { + @trigger_error(sprintf('Method "%s:createDir()" id deprecated. Use "%1$s:createDirectory()"', __CLASS__), \E_USER_DEPRECATED); + return $this->upload($this->normaliseDirName($dirname), '', $config); } + /** + * {@inheritdoc} + */ + public function createDirectory(string $dirname, Config $config): void + { + $this->upload($this->normaliseDirName($dirname), '', $config); + } + /** * Returns a normalised directory name from the given path. * @@ -299,45 +417,41 @@ protected function normaliseDirName($dirname) return rtrim($dirname, '/') . '/'; } + protected function normalizeDirname(string $dirname): string + { + return $dirname === '.' ? '' : $dirname; + } + /** * {@inheritdoc} */ - public function setVisibility($path, $visibility) + public function setVisibility(string $path, string $visibility): void { $object = $this->getObject($path); - if ($visibility === AdapterInterface::VISIBILITY_PRIVATE) { + if ($visibility === Visibility::PRIVATE) { $object->acl()->delete('allUsers'); - } elseif ($visibility === AdapterInterface::VISIBILITY_PUBLIC) { + } elseif ($visibility === Visibility::PUBLIC) { $object->acl()->add('allUsers', Acl::ROLE_READER); } - - $normalised = $this->normaliseObject($object); - $normalised['visibility'] = $visibility; - - return $normalised; } /** - * {@inheritdoc} + * @deprecated Use {@see self::fileExists() } */ public function has($path) { + @trigger_error(sprintf('Method "%s:has()" id deprecated. Use "%1$s:fileExists()"', __CLASS__), \E_USER_DEPRECATED); + return $this->getObject($path)->exists(); } /** * {@inheritdoc} */ - public function read($path) + public function read(string $path): string { - $object = $this->getObject($path); - $contents = $object->downloadAsString(); - - $data = $this->normaliseObject($object); - $data['contents'] = $contents; - - return $data; + return $this->getObject($path)->downloadAsString(); } /** @@ -347,16 +461,13 @@ public function readStream($path) { $object = $this->getObject($path); - $data = $this->normaliseObject($object); - $data['stream'] = StreamWrapper::getResource($object->downloadAsStream()); - - return $data; + return StreamWrapper::getResource($object->downloadAsStream()); } /** * {@inheritdoc} */ - public function listContents($directory = '', $recursive = false) + public function listContents(string $directory = '', bool $recursive = false): iterable { $directory = $this->applyPathPrefix($directory); @@ -367,7 +478,7 @@ public function listContents($directory = '', $recursive = false) $normalised[] = $this->normaliseObject($object); } - return Util::emulateDirectories($normalised); + return $this->emulateDirectories($normalised); } /** @@ -380,34 +491,42 @@ public function getMetadata($path) } /** - * {@inheritdoc} + * @deprecated Use {@see self::fileSize() } */ public function getSize($path) { + @trigger_error(sprintf('Method "%s:getSize()" id deprecated. Use "%1$s:fileSize()"', __CLASS__), \E_USER_DEPRECATED); + return $this->getMetadata($path); } /** - * {@inheritdoc} + * @deprecated Use {@see self::mimeType() } */ public function getMimetype($path) { + @trigger_error(sprintf('Method "%s:getMimetype()" id deprecated. Use "%1$s:mimeType()"', __CLASS__), \E_USER_DEPRECATED); + return $this->getMetadata($path); } /** - * {@inheritdoc} + * @deprecated Use {@see self::lastModified() } */ public function getTimestamp($path) { + @trigger_error(sprintf('Method "%s:getTimestamp()" id deprecated. Use "%1$s:lastModified()"', __CLASS__), \E_USER_DEPRECATED); + return $this->getMetadata($path); } /** - * {@inheritdoc} + * @deprecated Use {@see self::lastModified() } */ public function getVisibility($path) { + @trigger_error(sprintf('Method "%s:getVisibility()" id deprecated. Use "%1$s:visibility()"', __CLASS__), \E_USER_DEPRECATED); + return [ 'visibility' => $this->getRawVisibility($path), ]; @@ -416,7 +535,7 @@ public function getVisibility($path) /** * Return a public url to a file. * - * Note: The file must have `AdapterInterface::VISIBILITY_PUBLIC` visibility. + * Note: The file must have `Visibility::PUBLIC` visibility. * * @param string $path * @@ -497,6 +616,96 @@ public function getTemporaryUrl($path, $expiration, $options = []) return $signedUrl; } + /** + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. + */ + protected function basename($path) + { + $separators = DIRECTORY_SEPARATOR === '/' ? '/' : '\/'; + + $path = rtrim($path, $separators); + + $basename = preg_replace('#.*?([^' . preg_quote($separators, '#') . ']+$)#', '$1', $path); + + if (DIRECTORY_SEPARATOR === '/') { + return $basename; + } + // @codeCoverageIgnoreStart + // Extra Windows path munging. This is tested via AppVeyor, but code + // coverage is not reported. + + // Handle relative paths with drive letters. c:file.txt. + while (preg_match('#^[a-zA-Z]{1}:[^\\\/]#', $basename)) { + $basename = substr($basename, 2); + } + + // Remove colon for standalone drive letter names. + if (preg_match('#^[a-zA-Z]{1}:$#', $basename)) { + $basename = rtrim($basename, ':'); + } + + return $basename; + // @codeCoverageIgnoreEnd + } + + /** + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. + */ + protected function dirname(string $path): string + { + return $this->normalizeDirname(dirname($path)); + } + + /** + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. + */ + protected function emulateDirectories(array $listing): array + { + $directories = []; + $listedDirectories = []; + + foreach ($listing as $object) { + [$directories, $listedDirectories] = $this->emulateObjectDirectories($object, $directories, $listedDirectories); + } + + $directories = array_diff(array_unique($directories), array_unique($listedDirectories)); + + foreach ($directories as $directory) { + $listing[] = $this->pathinfo($directory) + ['type' => 'dir']; + } + + return $listing; + } + + /** + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. + */ + protected function emulateObjectDirectories(array $object, array $directories, array $listedDirectories): array + { + if ($object['type'] === 'dir') { + $listedDirectories[] = $object['path']; + } + + if ( ! isset($object['dirname']) || trim($object['dirname']) === '') { + return [$directories, $listedDirectories]; + } + + $parent = $object['dirname']; + + while (isset($parent) && trim($parent) !== '' && ! \in_array($parent, $directories, true)) { + $directories[] = $parent; + $parent = $this->dirname($parent); + } + + if (isset($object['type']) && $object['type'] === 'dir') { + $listedDirectories[] = $object['path']; + + return [$directories, $listedDirectories]; + } + + return [$directories, $listedDirectories]; + } + /** * @param string $path * @@ -506,12 +715,10 @@ protected function getRawVisibility($path) { try { $acl = $this->getObject($path)->acl()->get(['entity' => 'allUsers']); - return $acl['role'] === Acl::ROLE_READER ? - AdapterInterface::VISIBILITY_PUBLIC : - AdapterInterface::VISIBILITY_PRIVATE; + return $acl['role'] === Acl::ROLE_READER ? Visibility::PUBLIC : Visibility::PRIVATE; } catch (NotFoundException $e) { // object may not have an acl entry, so handle that gracefully - return AdapterInterface::VISIBILITY_PRIVATE; + return Visibility::PRIVATE; } } @@ -535,6 +742,24 @@ protected function getObject($path) */ protected function getPredefinedAclForVisibility($visibility) { - return $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'publicRead' : 'projectPrivate'; + return $visibility === Visibility::PUBLIC ? 'publicRead' : 'projectPrivate'; + } + + /** + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. + */ + protected function pathinfo($path) + { + $pathinfo = compact('path'); + + if ('' !== $dirname = dirname($path)) { + $pathinfo['dirname'] = $this->normalizeDirname($dirname); + } + + $pathinfo['basename'] = $this->basename($path); + + $pathinfo += pathinfo($pathinfo['basename']); + + return $pathinfo + ['dirname' => '']; } } diff --git a/tests/GoogleStorageAdapterTests.php b/tests/GoogleStorageAdapterTests.php index 450dc51..8cc72a4 100755 --- a/tests/GoogleStorageAdapterTests.php +++ b/tests/GoogleStorageAdapterTests.php @@ -1,23 +1,23 @@ createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket); $this->assertSame($storageClient, $adapter->getStorageClient()); @@ -25,8 +25,8 @@ public function testGetStorageClient() public function testGetBucket() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket); $this->assertSame($bucket, $adapter->getBucket()); @@ -34,36 +34,45 @@ public function testGetBucket() public function testWrite() { - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->exactly(2)) + ->method('name') + ->willReturn('prefix/file1.txt'); + $storageObject + ->expects($this->exactly(2)) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('upload') - ->withArgs([ + $bucket + ->expects($this->once()) + ->method('upload') + ->with( 'This is the file contents.', [ 'name' => 'prefix/file1.txt', 'predefinedAcl' => 'projectPrivate', ], - ]) - ->once() - ->andReturn($storageObject); + ) + ->willReturn($storageObject); + + $bucket + ->expects($this->once()) + ->method('object') + ->with('prefix/file1.txt', []) + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->write('file1.txt', 'This is the file contents.', new Config()); + $adapter->write('file1.txt', 'This is the file contents.', new Config()); $expected = [ 'type' => 'file', @@ -73,41 +82,50 @@ public function testWrite() 'mimetype' => 'text/plain', 'size' => 5, ]; - $this->assertEquals($expected, $data); + $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } public function testWriteWithPrivateVisibility() { - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->exactly(2)) + ->method('name') + ->willReturn('prefix/file1.txt'); + $storageObject + ->expects($this->exactly(2)) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('upload') - ->withArgs([ + $bucket + ->expects($this->once()) + ->method('upload') + ->with( 'This is the file contents.', [ 'name' => 'prefix/file1.txt', 'predefinedAcl' => 'projectPrivate', - ], - ]) - ->once() - ->andReturn($storageObject); + ] + ) + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $bucket + ->expects($this->once()) + ->method('object') + ->with('prefix/file1.txt', []) + ->willReturn($storageObject); + + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => AdapterInterface::VISIBILITY_PRIVATE])); + $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => Visibility::PRIVATE])); $expected = [ 'type' => 'file', @@ -117,41 +135,50 @@ public function testWriteWithPrivateVisibility() 'mimetype' => 'text/plain', 'size' => 5, ]; - $this->assertEquals($expected, $data); + $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } public function testWriteWithPublicVisibility() { - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->exactly(2)) + ->method('name') + ->willReturn('prefix/file1.txt'); + $storageObject + ->expects($this->exactly(2)) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('upload') - ->withArgs([ + $bucket + ->expects($this->once()) + ->method('upload') + ->with( 'This is the file contents.', [ 'name' => 'prefix/file1.txt', 'predefinedAcl' => 'publicRead', ], - ]) - ->once() - ->andReturn($storageObject); + ) + ->willReturn($storageObject); + + $bucket + ->expects($this->once()) + ->method('object') + ->with('prefix/file1.txt', []) + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => AdapterInterface::VISIBILITY_PUBLIC])); + $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => Visibility::PUBLIC])); $expected = [ 'type' => 'file', @@ -161,43 +188,51 @@ public function testWriteWithPublicVisibility() 'mimetype' => 'text/plain', 'size' => 5, ]; - $this->assertEquals($expected, $data); + $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } public function testWriteStream() { $stream = tmpfile(); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->exactly(2)) + ->method('name') + ->willReturn('prefix/file1.txt'); + $storageObject + ->expects($this->exactly(2)) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('upload') - ->withArgs([ + $bucket->expects($this->once()) + ->method('upload') + ->with( $stream, [ 'name' => 'prefix/file1.txt', 'predefinedAcl' => 'projectPrivate', ], - ]) - ->once() - ->andReturn($storageObject); + ) + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $bucket + ->expects($this->once()) + ->method('object') + ->with('prefix/file1.txt', []) + ->willReturn($storageObject); + + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->writeStream('file1.txt', $stream, new Config()); + $adapter->writeStream('file1.txt', $stream, new Config()); fclose($stream); @@ -209,137 +244,221 @@ public function testWriteStream() 'mimetype' => 'text/plain', 'size' => 5, ]; - $this->assertEquals($expected, $data); + $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } public function testRename() { - $bucket = Mockery::mock(Bucket::class); + $bucket = $this->createMock(Bucket::class); - $oldStorageObjectAcl = Mockery::mock(Acl::class); - $oldStorageObjectAcl->shouldReceive('get') + $oldStorageObjectAcl = $this->createMock(Acl::class); + $oldStorageObjectAcl + ->expects($this->once()) + ->method('get') ->with(['entity' => 'allUsers']) - ->once() - ->andReturn([ + ->willReturn([ 'role' => Acl::ROLE_OWNER, ]); - $oldStorageObject = Mockery::mock(StorageObject::class); - $oldStorageObject->shouldReceive('acl') - ->once() - ->andReturn($oldStorageObjectAcl); - $oldStorageObject->shouldReceive('copy') - ->withArgs([ + $newStorageObject = $this->createMock(StorageObject::class); + $newStorageObject + ->method('exists') + ->willReturn(true); + + $oldStorageObject = $this->createMock(StorageObject::class); + $oldStorageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($oldStorageObjectAcl); + $oldStorageObject + ->expects($this->once()) + ->method('copy') + ->with( $bucket, [ 'name' => 'prefix/new_file.txt', 'predefinedAcl' => 'projectPrivate', ], - ]) - ->once(); - $oldStorageObject->shouldReceive('delete') - ->once(); + ) + ->willReturn($newStorageObject); + $oldStorageObject + ->expects($this->exactly(0)) + ->method('delete'); + + $bucket + ->expects($this->exactly(2)) + ->method('object') + ->with('prefix/old_file.txt') + ->willReturn($oldStorageObject); + + $storageClient = $this->createMock(StorageClient::class); - $bucket->shouldReceive('object') + $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); + + $adapter->move('old_file.txt', 'new_file.txt', new Config()); + } + + public function testDeleteOnRename(): void + { + $bucket = $this->createMock(Bucket::class); + + $oldStorageObjectAcl = $this->createMock(Acl::class); + $oldStorageObjectAcl + ->expects($this->once()) + ->method('get') + ->with(['entity' => 'allUsers']) + ->willReturn([ + 'role' => Acl::ROLE_OWNER, + ]); + + $newStorageObject = $this->createMock(StorageObject::class); + $newStorageObject + ->method('exists') + ->willReturn(false); + + $oldStorageObject = $this->createMock(StorageObject::class); + $oldStorageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($oldStorageObjectAcl); + $oldStorageObject + ->expects($this->once()) + ->method('copy') + ->with( + $bucket, + [ + 'name' => 'prefix/new_file.txt', + 'predefinedAcl' => 'projectPrivate', + ], + ) + ->willReturn($newStorageObject); + $oldStorageObject + ->expects($this->once()) + ->method('delete'); + + $bucket + ->expects($this->exactly(3)) + ->method('object') ->with('prefix/old_file.txt') - ->times(3) - ->andReturn($oldStorageObject); + ->willReturn($oldStorageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->rename('old_file.txt', 'new_file.txt'); + $adapter->move('old_file.txt', 'new_file.txt', new Config()); } public function testCopy() { - $bucket = Mockery::mock(Bucket::class); + $bucket = $this->createMock(Bucket::class); - $oldStorageObjectAcl = Mockery::mock(Acl::class); - $oldStorageObjectAcl->shouldReceive('get') + $oldStorageObjectAcl = $this->createMock(Acl::class); + $oldStorageObjectAcl + ->expects($this->once()) + ->method('get') ->with(['entity' => 'allUsers']) - ->once() - ->andReturn([ + ->willReturn([ 'role' => Acl::ROLE_OWNER, ]); - $oldStorageObject = Mockery::mock(StorageObject::class); - $oldStorageObject->shouldReceive('acl') - ->once() - ->andReturn($oldStorageObjectAcl); - $oldStorageObject->shouldReceive('copy') - ->withArgs([ + $newStorageObject = $this->createMock(StorageObject::class); + $newStorageObject + ->method('exists') + ->willReturn(true); + + $oldStorageObject = $this->createMock(StorageObject::class); + $oldStorageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($oldStorageObjectAcl); + $oldStorageObject + ->expects($this->once()) + ->method('copy') + ->with( $bucket, [ 'name' => 'prefix/new_file.txt', 'predefinedAcl' => 'projectPrivate', ], - ]) - ->once(); + ) + ->willReturn($newStorageObject); - $bucket->shouldReceive('object') + $bucket + ->expects($this->exactly(2)) + ->method('object') ->with('prefix/old_file.txt') - ->times(2) - ->andReturn($oldStorageObject); + ->willReturn($oldStorageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->copy('old_file.txt', 'new_file.txt'); + $adapter->copy('old_file.txt', 'new_file.txt', new Config()); } public function testCopyWhenOriginalFileIsPublic() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); - $oldStorageObjectAcl = Mockery::mock(Acl::class); - $oldStorageObjectAcl->shouldReceive('get') + $oldStorageObjectAcl = $this->createMock(Acl::class); + $oldStorageObjectAcl + ->expects($this->once()) + ->method('get') ->with(['entity' => 'allUsers']) - ->once() - ->andReturn([ + ->willReturn([ 'role' => Acl::ROLE_READER, ]); - $oldStorageObject = Mockery::mock(StorageObject::class); - $oldStorageObject->shouldReceive('acl') - ->once() - ->andReturn($oldStorageObjectAcl); - $oldStorageObject->shouldReceive('copy') - ->withArgs([ + $newStorageObject = $this->createMock(StorageObject::class); + $newStorageObject + ->method('exists') + ->willReturn(true); + + $oldStorageObject = $this->createMock(StorageObject::class); + $oldStorageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($oldStorageObjectAcl); + $oldStorageObject + ->expects($this->once()) + ->method('copy') + ->with( $bucket, [ 'name' => 'prefix/new_file.txt', 'predefinedAcl' => 'publicRead', ], - ]) - ->once(); + ) + ->willReturn($newStorageObject); - $bucket->shouldReceive('object') + $bucket + ->expects($this->exactly(2)) + ->method('object') ->with('prefix/old_file.txt') - ->times(2) - ->andReturn($oldStorageObject); + ->willReturn($oldStorageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->copy('old_file.txt', 'new_file.txt'); + $adapter->copy('old_file.txt', 'new_file.txt', new Config()); } public function testDelete() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('delete') - ->once(); + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('delete'); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -348,280 +467,272 @@ public function testDelete() public function testDeleteDir() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('delete') - ->times(3); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/dir_name/directory1/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->exactly(3)) + ->method('delete'); + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/dir_name/directory1/file1.txt'); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') - ->with('prefix/dir_name/directory1/file1.txt') - ->once() - ->andReturn($storageObject); - - $bucket->shouldReceive('object') - ->with('prefix/dir_name/directory1/') - ->once() - ->andReturn($storageObject); + $bucket + ->expects($this->exactly(3)) + ->method('object') + ->withConsecutive(['prefix/dir_name/directory1/file1.txt', []], ['prefix/dir_name/directory1/', []], ['prefix/dir_name/', []]) + ->willReturnOnConsecutiveCalls($storageObject, $storageObject, $storageObject); - $bucket->shouldReceive('object') - ->with('prefix/dir_name/') - ->once() - ->andReturn($storageObject); - - $bucket->shouldReceive('objects') + $bucket + ->expects($this->once()) + ->method('objects') ->with([ 'prefix' => 'prefix/dir_name/' - ])->once() - ->andReturn([$storageObject]); + ]) + ->willReturn([$storageObject]); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->deleteDir('dir_name'); + $adapter->deleteDirectory('dir_name'); } public function testDeleteDirWithTrailingSlash() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('delete') - ->times(3); - - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/dir_name/directory1/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->exactly(3)) + ->method('delete'); + + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/dir_name/directory1/file1.txt', []); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') - ->with('prefix/dir_name/directory1/file1.txt') - ->once() - ->andReturn($storageObject); - - $bucket->shouldReceive('object') - ->with('prefix/dir_name/directory1/') - ->once() - ->andReturn($storageObject); + $bucket + ->expects($this->exactly(3)) + ->method('object') + ->withConsecutive(['prefix/dir_name/directory1/file1.txt', []], ['prefix/dir_name/directory1/', []], ['prefix/dir_name/', []]) + ->willReturnOnConsecutiveCalls($storageObject, $storageObject, $storageObject); - $bucket->shouldReceive('object') - ->with('prefix/dir_name/') - ->once() - ->andReturn($storageObject); - - $bucket->shouldReceive('objects') + $bucket + ->expects($this->once()) + ->method('objects') ->with([ 'prefix' => 'prefix/dir_name/' - ])->once() - ->andReturn([$storageObject]); + ]) + ->willReturn([$storageObject]); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->deleteDir('dir_name//'); + $adapter->deleteDirectory('dir_name//'); } public function testSetVisibilityPrivate() { - $bucket = Mockery::mock(Bucket::class); - - $storageObjectAcl = Mockery::mock(Acl::class); - $storageObjectAcl->shouldReceive('delete') - ->with('allUsers') - ->once(); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('acl') - ->once() - ->andReturn($storageObjectAcl); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $bucket = $this->createMock(Bucket::class); + + $storageObjectAcl = $this->createMock(Acl::class); + $storageObjectAcl + ->expects($this->once()) + ->method('delete') + ->with('allUsers'); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($storageObjectAcl); + $storageObject + ->expects($this->exactly(0)) + ->method('name') + ->willReturn('prefix/file.txt'); + $storageObject + ->expects($this->exactly(0)) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file1.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->setVisibility('file1.txt', AdapterInterface::VISIBILITY_PRIVATE); - $this->assertArrayHasKey('visibility', $data); - $this->assertEquals(AdapterInterface::VISIBILITY_PRIVATE, $data['visibility']); + $adapter->setVisibility('file1.txt', Visibility::PRIVATE); } public function testSetVisibilityPublic() { - $bucket = Mockery::mock(Bucket::class); + $bucket = $this->createMock(Bucket::class); - $storageObjectAcl = Mockery::mock(Acl::class); - $storageObjectAcl->shouldReceive('add') - ->withArgs([ + $storageObjectAcl = $this->createMock(Acl::class); + $storageObjectAcl + ->expects($this->once()) + ->method('add') + ->with( 'allUsers', Acl::ROLE_READER, - ]) - ->once(); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('acl') - ->once() - ->andReturn($storageObjectAcl); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket->shouldReceive('object') + ); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($storageObjectAcl); + $storageObject + ->expects($this->exactly(0)) + ->method('name'); + $storageObject + ->expects($this->exactly(0)) + ->method('info'); + + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file1.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->setVisibility('file1.txt', AdapterInterface::VISIBILITY_PUBLIC); - $this->assertArrayHasKey('visibility', $data); - $this->assertEquals(AdapterInterface::VISIBILITY_PUBLIC, $data['visibility']); + $adapter->setVisibility('file1.txt', Visibility::PUBLIC); } public function testHas() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('exists') - ->once(); - - $bucket->shouldReceive('object') + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('exists') + ->willReturn(true); + + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->has('file.txt'); + self::assertTrue($adapter->fileExists('file.txt')); } public function testRead() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('downloadAsString') - ->once() - ->andReturn('This is the file contents.'); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket->shouldReceive('object') + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('downloadAsString') + ->willReturn('This is the file contents.'); + $storageObject + ->expects($this->exactly(0)) + ->method('name'); + $storageObject + ->expects($this->exactly(0)) + ->method('info'); + + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); $data = $adapter->read('file.txt'); - $this->assertArrayHasKey('contents', $data); - $this->assertEquals('This is the file contents.', $data['contents']); + $this->assertEquals('This is the file contents.', $data); } public function testReadStream() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $stream = Mockery::mock(StreamInterface::class); - $stream->shouldReceive('isReadable') - ->once() - ->andReturn(true); - $stream->shouldReceive('isWritable') - ->once() - ->andReturn(false); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('downloadAsStream') - ->once() - ->andReturn($stream); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket->shouldReceive('object') + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $stream = $this->createMock(StreamInterface::class); + $stream + ->expects($this->once()) + ->method('isReadable') + ->willReturn(true); + $stream + ->expects($this->once()) + ->method('isWritable') + ->willReturn(false); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('downloadAsStream') + ->willReturn($stream); + $storageObject + ->expects($this->exactly(0)) + ->method('name'); + $storageObject + ->expects($this->exactly(0)) + ->method('info'); + + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); $data = $adapter->readStream('file.txt'); - $this->assertArrayHasKey('stream', $data); - $this->assertInternalType('resource', $data['stream']); + $this->assertIsResource($data); } public function testListContents() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); $prefix = 'prefix/'; - $bucket->shouldReceive('objects') - ->once() + $bucket + ->expects($this->once()) + ->method('objects') ->with([ 'prefix' => $prefix, ]) - ->andReturn($this->getMockDirObjects($prefix)); + ->willReturn($this->getMockDirObjects($prefix)); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -665,43 +776,47 @@ public function testListContents() } /** - * @param string $prefix - * - * @return array + * @return StorageObject[] */ - protected function getMockDirObjects($prefix = '') + protected function getMockDirObjects(string $prefix): array { - $dir1 = Mockery::mock(StorageObject::class); - $dir1->shouldReceive('name') - ->once() - ->andReturn($prefix . 'directory1/'); - $dir1->shouldReceive('info') - ->once() - ->andReturn([ + $dir1 = $this->createMock(StorageObject::class); + $dir1 + ->expects($this->once()) + ->method('name') + ->willReturn($prefix . 'directory1/'); + $dir1 + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'application/octet-stream', 'size' => 0, ]); - $dir1file1 = Mockery::mock(StorageObject::class); - $dir1file1->shouldReceive('name') - ->once() - ->andReturn($prefix . 'directory1/file1.txt'); - $dir1file1->shouldReceive('info') - ->once() - ->andReturn([ + $dir1file1 = $this->createMock(StorageObject::class); + $dir1file1 + ->expects($this->once()) + ->method('name') + ->willReturn($prefix . 'directory1/file1.txt'); + $dir1file1 + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $dir2file1 = Mockery::mock(StorageObject::class); - $dir2file1->shouldReceive('name') - ->once() - ->andReturn($prefix . 'directory2/file1.txt'); - $dir2file1->shouldReceive('info') - ->once() - ->andReturn([ + $dir2file1 = $this->createMock(StorageObject::class); + $dir2file1 + ->expects($this->once()) + ->method('name') + ->willReturn($prefix . 'directory2/file1.txt'); + $dir2file1 + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, @@ -716,25 +831,28 @@ protected function getMockDirObjects($prefix = '') public function testGetMetadataForFile() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/file.txt'); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -754,25 +872,28 @@ public function testGetMetadataForFile() public function testGetMetadataForDir() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/directory/'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/directory/'); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'application/octet-stream', 'size' => 0, ]); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/directory') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -792,25 +913,28 @@ public function testGetMetadataForDir() public function testGetSize() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/file.txt'); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -822,25 +946,28 @@ public function testGetSize() public function testGetMimetype() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/file.txt'); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -852,25 +979,28 @@ public function testGetMimetype() public function testGetTimestamp() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('prefix/file.txt'); + $storageObject + ->expects($this->once()) + ->method('info') + ->willReturn([ 'updated' => '2016-09-26T14:44:42+00:00', 'contentType' => 'text/plain', 'size' => 5, ]); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); @@ -882,68 +1012,74 @@ public function testGetTimestamp() public function testGetVisibilityWhenVisibilityIsPrivate() { - $bucket = Mockery::mock(Bucket::class); + $bucket = $this->createMock(Bucket::class); - $storageObjectAcl = Mockery::mock(Acl::class); - $storageObjectAcl->shouldReceive('get') + $storageObjectAcl = $this->createMock(Acl::class); + $storageObjectAcl + ->expects($this->once()) + ->method('get') ->with(['entity' => 'allUsers']) - ->once() - ->andReturn([ + ->willReturn([ 'role' => Acl::ROLE_OWNER, ]); - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('acl') - ->once() - ->andReturn($storageObjectAcl); + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($storageObjectAcl); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $visibility = $adapter->getVisibility('file.txt'); - $this->assertEquals(['visibility' => AdapterInterface::VISIBILITY_PRIVATE], $visibility); + $attributes = $adapter->visibility('file.txt'); + $this->assertEquals(Visibility::PRIVATE, $attributes->visibility()); } public function testGetVisibilityWhenVisibilityIsPublic() { - $bucket = Mockery::mock(Bucket::class); + $bucket = $this->createMock(Bucket::class); - $storageObjectAcl = Mockery::mock(Acl::class); - $storageObjectAcl->shouldReceive('get') + $storageObjectAcl = $this->createMock(Acl::class); + $storageObjectAcl + ->expects($this->once()) + ->method('get') ->with(['entity' => 'allUsers']) - ->once() - ->andReturn([ + ->willReturn([ 'role' => Acl::ROLE_READER, ]); - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('acl') - ->once() - ->andReturn($storageObjectAcl); + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('acl') + ->willReturn($storageObjectAcl); - $bucket->shouldReceive('object') + $bucket + ->expects($this->once()) + ->method('object') ->with('prefix/file.txt') - ->once() - ->andReturn($storageObject); + ->willReturn($storageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $visibility = $adapter->getVisibility('file.txt'); - $this->assertEquals(['visibility' => AdapterInterface::VISIBILITY_PUBLIC], $visibility); + $attributes = $adapter->visibility('file.txt'); + $this->assertEquals(Visibility::PUBLIC, $attributes->visibility()); } public function testSetGetStorageApiUri() { - $storageClient = Mockery::mock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket); $this->assertEquals('https://storage.googleapis.com', $adapter->getStorageApiUri()); @@ -957,11 +1093,13 @@ public function testSetGetStorageApiUri() public function testGetUrl() { - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); - $bucket = Mockery::mock(Bucket::class); - $bucket->shouldReceive('name') - ->andReturn('my-bucket'); + $bucket = $this->createMock(Bucket::class); + $bucket + ->expects($this->exactly(3)) + ->method('name') + ->willReturn('my-bucket'); $adapter = new GoogleStorageAdapter($storageClient, $bucket); $this->assertEquals('https://storage.googleapis.com/my-bucket/file.txt', $adapter->getUrl('file.txt')); From edeae5775f00c9610fd848d101582e9aecae7b63 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 22 Dec 2022 16:02:51 +0300 Subject: [PATCH 2/6] Require roave/security-advisories:dev-latest to prevent requiring of not secure dependencies --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 082ba73..c8c68ee 100755 --- a/composer.json +++ b/composer.json @@ -14,7 +14,8 @@ "google/cloud-storage": "~1.0" }, "require-dev": { - "phpunit/phpunit": "^9.5" + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-latest" }, "autoload": { "psr-4": { From 79e134b8835f036877b30152ad2a4e5ce49190a0 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 22 Dec 2022 16:07:27 +0300 Subject: [PATCH 3/6] Configure sorting of dependencies --- composer.json | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index c8c68ee..a7ff108 100755 --- a/composer.json +++ b/composer.json @@ -10,8 +10,8 @@ ], "require": { "php": "^7.4", - "league/flysystem": "^2.0", - "google/cloud-storage": "~1.0" + "google/cloud-storage": "~1.0", + "league/flysystem": "^2.0" }, "require-dev": { "phpunit/phpunit": "^9.5", @@ -28,11 +28,6 @@ } }, "config": { - "bin-dir": "bin" - }, - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } + "sort-packages": true } } From a9c3bcd1761476ac99348af892f60155f086430f Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 22 Dec 2022 16:39:38 +0300 Subject: [PATCH 4/6] Declare data types --- src/GoogleStorageAdapter.php | 217 +++++++++++++--------------- tests/GoogleStorageAdapterTests.php | 55 ++++--- 2 files changed, 130 insertions(+), 142 deletions(-) diff --git a/src/GoogleStorageAdapter.php b/src/GoogleStorageAdapter.php index e228fc0..d2f8330 100755 --- a/src/GoogleStorageAdapter.php +++ b/src/GoogleStorageAdapter.php @@ -19,40 +19,15 @@ class GoogleStorageAdapter implements FilesystemAdapter /** * @const STORAGE_API_URI_DEFAULT */ - const STORAGE_API_URI_DEFAULT = 'https://storage.googleapis.com'; + public const STORAGE_API_URI_DEFAULT = 'https://storage.googleapis.com'; - /** - * @var StorageClient - */ - protected $storageClient; - - /** - * @var Bucket - */ - protected $bucket; - - /** - * @var string|null path prefix - */ + protected StorageClient $storageClient; + protected Bucket $bucket; protected ?string $pathPrefix = null; - - /** - * @var string - */ protected string $pathSeparator = '/'; + protected string $storageApiUri; - /** - * @var string - */ - protected $storageApiUri; - - /** - * @param StorageClient $storageClient - * @param Bucket $bucket - * @param string $pathPrefix - * @param string $storageApiUri - */ - public function __construct(StorageClient $storageClient, Bucket $bucket, $pathPrefix = null, $storageApiUri = null) + public function __construct(StorageClient $storageClient, Bucket $bucket, string $pathPrefix = null, string $storageApiUri = null) { $this->storageClient = $storageClient; $this->bucket = $bucket; @@ -130,42 +105,22 @@ public function setPathPrefix(?string $prefix): void $this->pathPrefix = rtrim($prefix, '\\/') . $this->pathSeparator; } - /** - * Returns the StorageClient. - * - * @return StorageClient - */ - public function getStorageClient() + public function getStorageClient(): StorageClient { return $this->storageClient; } - /** - * Return the Bucket. - * - * @return \Google\Cloud\Storage\Bucket - */ - public function getBucket() + public function getBucket(): Bucket { return $this->bucket; } - /** - * Set the storage api uri. - * - * @param string $uri - */ - public function setStorageApiUri($uri) + public function setStorageApiUri(string $uri): void { $this->storageApiUri = $uri; } - /** - * Return the storage api uri. - * - * @return string - */ - public function getStorageApiUri() + public function getStorageApiUri(): string { return $this->storageApiUri; } @@ -215,21 +170,17 @@ public function writeStream(string $path, $resource, Config $config): void } /** - * {@inheritdoc} - * * @codeCoverageIgnore */ - public function update($path, $contents, Config $config) + public function update(string $path, $contents, Config $config): array { return $this->upload($path, $contents, $config); } /** - * {@inheritdoc} - * * @codeCoverageIgnore */ - public function updateStream($path, $resource, Config $config) + public function updateStream(string $path, $resource, Config $config): array { return $this->upload($path, $resource, $config); } @@ -237,11 +188,9 @@ public function updateStream($path, $resource, Config $config) /** * Returns an array of options from the config. * - * @param Config $config - * - * @return array + * @return array */ - protected function getOptionsFromConfig(Config $config) + protected function getOptionsFromConfig(Config $config): array { $options = []; @@ -263,13 +212,18 @@ protected function getOptionsFromConfig(Config $config) /** * Uploads a file to the Google Cloud Storage service. * - * @param string $path * @param string|resource $contents - * @param Config $config * - * @return array + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - protected function upload($path, $contents, Config $config) + protected function upload(string $path, $contents, Config $config): array { $path = $this->applyPathPrefix($path); @@ -284,11 +238,16 @@ protected function upload($path, $contents, Config $config) /** * Returns a dictionary of object metadata from an object. * - * @param StorageObject $object - * - * @return array + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - protected function normaliseObject(StorageObject $object) + protected function normaliseObject(StorageObject $object): array { $name = $this->removePathPrefix($object->name()); $info = $object->info(); @@ -349,8 +308,9 @@ public function delete(string $path): void /** * @deprecated Use {@see self::deleteDirectory() } + * @codeCoverageIgnore */ - public function deleteDir($dirname) + public function deleteDir(string $dirname): void { @trigger_error(sprintf('Method "%s:deleteDir()" id deprecated. Use "%1$s:deleteDirectory()"', __CLASS__), \E_USER_DEPRECATED); @@ -389,8 +349,18 @@ public function deleteDirectory(string $dirname): void /** * @deprecated Use {@see self::createDirectory() } + * @codeCoverageIgnore + * + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - public function createDir($dirname, Config $config) + public function createDir(string $dirname, Config $config): array { @trigger_error(sprintf('Method "%s:createDir()" id deprecated. Use "%1$s:createDirectory()"', __CLASS__), \E_USER_DEPRECATED); @@ -407,12 +377,8 @@ public function createDirectory(string $dirname, Config $config): void /** * Returns a normalised directory name from the given path. - * - * @param string $dirname - * - * @return string */ - protected function normaliseDirName($dirname) + protected function normaliseDirName(string $dirname): string { return rtrim($dirname, '/') . '/'; } @@ -438,8 +404,9 @@ public function setVisibility(string $path, string $visibility): void /** * @deprecated Use {@see self::fileExists() } + * @codeCoverageIgnore */ - public function has($path) + public function has(string $path): bool { @trigger_error(sprintf('Method "%s:has()" id deprecated. Use "%1$s:fileExists()"', __CLASS__), \E_USER_DEPRECATED); @@ -457,7 +424,7 @@ public function read(string $path): string /** * {@inheritdoc} */ - public function readStream($path) + public function readStream(string $path) { $object = $this->getObject($path); @@ -482,18 +449,36 @@ public function listContents(string $directory = '', bool $recursive = false): i } /** - * {@inheritdoc} + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - public function getMetadata($path) + public function getMetadata(string $path): array { $object = $this->getObject($path); + return $this->normaliseObject($object); } /** * @deprecated Use {@see self::fileSize() } + * @codeCoverageIgnore + * + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - public function getSize($path) + public function getSize(string $path): array { @trigger_error(sprintf('Method "%s:getSize()" id deprecated. Use "%1$s:fileSize()"', __CLASS__), \E_USER_DEPRECATED); @@ -502,8 +487,18 @@ public function getSize($path) /** * @deprecated Use {@see self::mimeType() } + * @codeCoverageIgnore + * + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - public function getMimetype($path) + public function getMimetype(string $path): array { @trigger_error(sprintf('Method "%s:getMimetype()" id deprecated. Use "%1$s:mimeType()"', __CLASS__), \E_USER_DEPRECATED); @@ -512,8 +507,18 @@ public function getMimetype($path) /** * @deprecated Use {@see self::lastModified() } + * @codeCoverageIgnore + * + * @return array{ + * type: string + * dirname: string + * path: string + * timestamp: int + * mimetype: string + * size: int + * } */ - public function getTimestamp($path) + public function getTimestamp(string $path): array { @trigger_error(sprintf('Method "%s:getTimestamp()" id deprecated. Use "%1$s:lastModified()"', __CLASS__), \E_USER_DEPRECATED); @@ -522,8 +527,11 @@ public function getTimestamp($path) /** * @deprecated Use {@see self::lastModified() } + * @codeCoverageIgnore + * + * @return array { visibility: string } */ - public function getVisibility($path) + public function getVisibility(string $path): array { @trigger_error(sprintf('Method "%s:getVisibility()" id deprecated. Use "%1$s:visibility()"', __CLASS__), \E_USER_DEPRECATED); @@ -536,12 +544,8 @@ public function getVisibility($path) * Return a public url to a file. * * Note: The file must have `Visibility::PUBLIC` visibility. - * - * @param string $path - * - * @return string */ - public function getUrl($path) + public function getUrl(string $path): string { $uri = rtrim($this->storageApiUri, '/'); $path = $this->applyPathPrefix($path); @@ -563,7 +567,7 @@ public function getUrl($path) /** * Get a temporary URL (Signed) for the file at the given path. - * @param string $path + * * @param \DateTimeInterface|int $expiration Specifies when the URL * will expire. May provide an instance of [http://php.net/datetimeimmutable](`\DateTimeImmutable`), * or a UNIX timestamp as an integer. @@ -601,9 +605,8 @@ public function getUrl($path) * @type bool $forceOpenssl If true, OpenSSL will be used regardless of * whether phpseclib is available. **Defaults to** `false`. * } - * @return string */ - public function getTemporaryUrl($path, $expiration, $options = []) + public function getTemporaryUrl(string $path, $expiration, array $options = []): string { $object = $this->getObject($path); $signedUrl = $object->signedUrl($expiration, $options); @@ -619,7 +622,7 @@ public function getTemporaryUrl($path, $expiration, $options = []) /** * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. */ - protected function basename($path) + protected function basename(string $path): string { $separators = DIRECTORY_SEPARATOR === '/' ? '/' : '\/'; @@ -706,12 +709,7 @@ protected function emulateObjectDirectories(array $object, array $directories, a return [$directories, $listedDirectories]; } - /** - * @param string $path - * - * @return string - */ - protected function getRawVisibility($path) + protected function getRawVisibility(string $path): string { try { $acl = $this->getObject($path)->acl()->get(['entity' => 'allUsers']); @@ -724,23 +722,14 @@ protected function getRawVisibility($path) /** * Returns a storage object for the given path. - * - * @param string $path - * - * @return \Google\Cloud\Storage\StorageObject */ - protected function getObject($path) + protected function getObject(string $path): StorageObject { $path = $this->applyPathPrefix($path); return $this->bucket->object($path); } - /** - * @param string $visibility - * - * @return string - */ - protected function getPredefinedAclForVisibility($visibility) + protected function getPredefinedAclForVisibility(string $visibility): string { return $visibility === Visibility::PUBLIC ? 'publicRead' : 'projectPrivate'; } @@ -748,7 +737,7 @@ protected function getPredefinedAclForVisibility($visibility) /** * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. */ - protected function pathinfo($path) + protected function pathinfo(string $path): array { $pathinfo = compact('path'); diff --git a/tests/GoogleStorageAdapterTests.php b/tests/GoogleStorageAdapterTests.php index 8cc72a4..d76dc16 100755 --- a/tests/GoogleStorageAdapterTests.php +++ b/tests/GoogleStorageAdapterTests.php @@ -14,7 +14,7 @@ class GoogleStorageAdapterTests extends TestCase { - public function testGetStorageClient() + public function testGetStorageClient(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -23,7 +23,7 @@ public function testGetStorageClient() $this->assertSame($storageClient, $adapter->getStorageClient()); } - public function testGetBucket() + public function testGetBucket(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -32,7 +32,7 @@ public function testGetBucket() $this->assertSame($bucket, $adapter->getBucket()); } - public function testWrite() + public function testWrite(): void { $bucket = $this->createMock(Bucket::class); @@ -85,7 +85,7 @@ public function testWrite() $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } - public function testWriteWithPrivateVisibility() + public function testWriteWithPrivateVisibility(): void { $bucket = $this->createMock(Bucket::class); @@ -138,7 +138,7 @@ public function testWriteWithPrivateVisibility() $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } - public function testWriteWithPublicVisibility() + public function testWriteWithPublicVisibility(): void { $bucket = $this->createMock(Bucket::class); @@ -191,7 +191,7 @@ public function testWriteWithPublicVisibility() $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } - public function testWriteStream() + public function testWriteStream(): void { $stream = tmpfile(); @@ -247,7 +247,7 @@ public function testWriteStream() $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } - public function testRename() + public function testRename(): void { $bucket = $this->createMock(Bucket::class); @@ -349,7 +349,7 @@ public function testDeleteOnRename(): void $adapter->move('old_file.txt', 'new_file.txt', new Config()); } - public function testCopy() + public function testCopy(): void { $bucket = $this->createMock(Bucket::class); @@ -397,7 +397,7 @@ public function testCopy() $adapter->copy('old_file.txt', 'new_file.txt', new Config()); } - public function testCopyWhenOriginalFileIsPublic() + public function testCopyWhenOriginalFileIsPublic(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -444,7 +444,7 @@ public function testCopyWhenOriginalFileIsPublic() $adapter->copy('old_file.txt', 'new_file.txt', new Config()); } - public function testDelete() + public function testDelete(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -465,7 +465,7 @@ public function testDelete() $adapter->delete('file.txt'); } - public function testDeleteDir() + public function testDeleteDir(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -506,7 +506,7 @@ public function testDeleteDir() $adapter->deleteDirectory('dir_name'); } - public function testDeleteDirWithTrailingSlash() + public function testDeleteDirWithTrailingSlash(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -547,8 +547,7 @@ public function testDeleteDirWithTrailingSlash() $adapter->deleteDirectory('dir_name//'); } - - public function testSetVisibilityPrivate() + public function testSetVisibilityPrivate(): void { $bucket = $this->createMock(Bucket::class); @@ -589,7 +588,7 @@ public function testSetVisibilityPrivate() $adapter->setVisibility('file1.txt', Visibility::PRIVATE); } - public function testSetVisibilityPublic() + public function testSetVisibilityPublic(): void { $bucket = $this->createMock(Bucket::class); @@ -627,7 +626,7 @@ public function testSetVisibilityPublic() $adapter->setVisibility('file1.txt', Visibility::PUBLIC); } - public function testHas() + public function testFileExists(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -649,7 +648,7 @@ public function testHas() self::assertTrue($adapter->fileExists('file.txt')); } - public function testRead() + public function testRead(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -679,7 +678,7 @@ public function testRead() $this->assertEquals('This is the file contents.', $data); } - public function testReadStream() + public function testReadStream(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -719,7 +718,7 @@ public function testReadStream() $this->assertIsResource($data); } - public function testListContents() + public function testListContents(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -829,7 +828,7 @@ protected function getMockDirObjects(string $prefix): array ]; } - public function testGetMetadataForFile() + public function testGetMetadataForFile(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -870,7 +869,7 @@ public function testGetMetadataForFile() $this->assertEquals($expected, $metadata); } - public function testGetMetadataForDir() + public function testGetMetadataForDir(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -911,7 +910,7 @@ public function testGetMetadataForDir() $this->assertEquals($expected, $metadata); } - public function testGetSize() + public function testGetSize(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -944,7 +943,7 @@ public function testGetSize() $this->assertEquals(5, $metadata['size']); } - public function testGetMimetype() + public function testGetMimetype(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -977,7 +976,7 @@ public function testGetMimetype() $this->assertEquals('text/plain', $metadata['mimetype']); } - public function testGetTimestamp() + public function testGetTimestamp(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -1010,7 +1009,7 @@ public function testGetTimestamp() $this->assertEquals(1474901082, $metadata['timestamp']); } - public function testGetVisibilityWhenVisibilityIsPrivate() + public function testGetVisibilityWhenVisibilityIsPrivate(): void { $bucket = $this->createMock(Bucket::class); @@ -1043,7 +1042,7 @@ public function testGetVisibilityWhenVisibilityIsPrivate() $this->assertEquals(Visibility::PRIVATE, $attributes->visibility()); } - public function testGetVisibilityWhenVisibilityIsPublic() + public function testGetVisibilityWhenVisibilityIsPublic(): void { $bucket = $this->createMock(Bucket::class); @@ -1076,7 +1075,7 @@ public function testGetVisibilityWhenVisibilityIsPublic() $this->assertEquals(Visibility::PUBLIC, $attributes->visibility()); } - public function testSetGetStorageApiUri() + public function testSetGetStorageApiUri(): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -1091,7 +1090,7 @@ public function testSetGetStorageApiUri() $this->assertEquals('http://this.is.my.base.com', $adapter->getStorageApiUri()); } - public function testGetUrl() + public function testGetUrl(): void { $storageClient = $this->createMock(StorageClient::class); From 5b58444bbc538ece57f491383a338f78da047ea4 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 22 Dec 2022 17:14:22 +0300 Subject: [PATCH 5/6] Refactor code --- src/GoogleStorageAdapter.php | 114 ++++++++------- tests/GoogleStorageAdapterTests.php | 207 ++++++---------------------- 2 files changed, 101 insertions(+), 220 deletions(-) diff --git a/src/GoogleStorageAdapter.php b/src/GoogleStorageAdapter.php index d2f8330..4a9a4ac 100755 --- a/src/GoogleStorageAdapter.php +++ b/src/GoogleStorageAdapter.php @@ -16,9 +16,6 @@ class GoogleStorageAdapter implements FilesystemAdapter { - /** - * @const STORAGE_API_URI_DEFAULT - */ public const STORAGE_API_URI_DEFAULT = 'https://storage.googleapis.com'; protected StorageClient $storageClient; @@ -27,8 +24,12 @@ class GoogleStorageAdapter implements FilesystemAdapter protected string $pathSeparator = '/'; protected string $storageApiUri; - public function __construct(StorageClient $storageClient, Bucket $bucket, string $pathPrefix = null, string $storageApiUri = null) - { + public function __construct( + StorageClient $storageClient, + Bucket $bucket, + string $pathPrefix = null, + string $storageApiUri = null + ) { $this->storageClient = $storageClient; $this->bucket = $bucket; @@ -164,9 +165,9 @@ public function write(string $path, string $contents, Config $config): void /** * {@inheritdoc} */ - public function writeStream(string $path, $resource, Config $config): void + public function writeStream(string $path, $contents, Config $config): void { - $this->upload($path, $resource, $config); + $this->upload($path, $contents, $config); } /** @@ -193,14 +194,10 @@ public function updateStream(string $path, $resource, Config $config): array protected function getOptionsFromConfig(Config $config): array { $options = []; - - if ($visibility = $config->get('visibility')) { - $options['predefinedAcl'] = $this->getPredefinedAclForVisibility($visibility); - } else { - // if a file is created without an acl, it isn't accessible via the console - // we therefore default to private - $options['predefinedAcl'] = $this->getPredefinedAclForVisibility(Visibility::PRIVATE); - } + // if a file is created without an acl, it isn't accessible via the console + // we therefore default to private + $visibility = $config->get('visibility') ?: Visibility::PRIVATE; + $options['predefinedAcl'] = $this->getPredefinedAclForVisibility($visibility); if ($metadata = $config->get('metadata')) { $options['metadata'] = $metadata; @@ -262,7 +259,7 @@ protected function normaliseObject(StorageObject $object): array 'dirname' => $this->dirname($name), 'path' => $name, 'timestamp' => strtotime($info['updated']), - 'mimetype' => isset($info['contentType']) ? $info['contentType'] : '', + 'mimetype' => $info['contentType'] ?? '', 'size' => $info['size'], ]; } @@ -274,7 +271,7 @@ public function move(string $source, string $destination, Config $config): void { try { $this->copy($source, $destination, $config); - } catch (UnableToCopyFile $exception){ + } catch (UnableToCopyFile $exception) { $this->delete($source); } } @@ -282,19 +279,19 @@ public function move(string $source, string $destination, Config $config): void /** * {@inheritdoc} */ - public function copy(string $path, string $newpath, Config $config): void + public function copy(string $source, string $destination, Config $config): void { - $newpath = $this->applyPathPrefix($newpath); + $destination = $this->applyPathPrefix($destination); // we want the new file to have the same visibility as the original file - $visibility = $this->getRawVisibility($path); + $visibility = $this->getRawVisibility($source); $options = [ - 'name' => $newpath, + 'name' => $destination, 'predefinedAcl' => $this->getPredefinedAclForVisibility($visibility), ]; - if (!$this->getObject($path)->copy($this->bucket, $options)->exists()) { - throw UnableToCopyFile::fromLocationTo($path, $newpath); + if (!$this->getObject($source)->copy($this->bucket, $options)->exists()) { + throw UnableToCopyFile::fromLocationTo($source, $destination); } } @@ -317,14 +314,14 @@ public function deleteDir(string $dirname): void $this->deleteDirectory($dirname); } - public function deleteDirectory(string $dirname): void + public function deleteDirectory(string $path): void { - $dirname = $this->normaliseDirName($dirname); - $objects = $this->listContents($dirname, true); + $path = $this->normalizeDirPostfix($path); + $objects = $this->listContents($path, true); // We first delete the file, so that we can delete // the empty folder at the end. - uasort($objects, function ($a, $b) { + uasort($objects, static function (array $a, array $b): int { return $b['type'] === 'file' ? 1 : -1; }); @@ -333,10 +330,10 @@ public function deleteDirectory(string $dirname): void foreach ($objects as $object) { // normalise directories path if ($object['type'] === 'dir') { - $object['path'] = $this->normaliseDirName($object['path']); + $object['path'] = $this->normalizeDirPostfix($object['path']); } - if (strpos($object['path'], $dirname) !== false) { + if (strpos($object['path'], $path) !== false) { $filtered_objects[] = $object; } } @@ -364,28 +361,15 @@ public function createDir(string $dirname, Config $config): array { @trigger_error(sprintf('Method "%s:createDir()" id deprecated. Use "%1$s:createDirectory()"', __CLASS__), \E_USER_DEPRECATED); - return $this->upload($this->normaliseDirName($dirname), '', $config); + return $this->upload($this->normalizeDirPostfix($dirname), '', $config); } /** * {@inheritdoc} */ - public function createDirectory(string $dirname, Config $config): void - { - $this->upload($this->normaliseDirName($dirname), '', $config); - } - - /** - * Returns a normalised directory name from the given path. - */ - protected function normaliseDirName(string $dirname): string - { - return rtrim($dirname, '/') . '/'; - } - - protected function normalizeDirname(string $dirname): string + public function createDirectory(string $path, Config $config): void { - return $dirname === '.' ? '' : $dirname; + $this->upload($this->normalizeDirPostfix($path), '', $config); } /** @@ -434,11 +418,11 @@ public function readStream(string $path) /** * {@inheritdoc} */ - public function listContents(string $directory = '', bool $recursive = false): iterable + public function listContents(string $path = '', bool $deep = false): iterable { - $directory = $this->applyPathPrefix($directory); + $path = $this->applyPathPrefix($path); - $objects = $this->bucket->objects(['prefix' => $directory]); + $objects = $this->bucket->objects(['prefix' => $path]); $normalised = []; foreach ($objects as $object) { @@ -612,7 +596,7 @@ public function getTemporaryUrl(string $path, $expiration, array $options = []): $signedUrl = $object->signedUrl($expiration, $options); if ($this->getStorageApiUri() !== self::STORAGE_API_URI_DEFAULT) { - list($url, $params) = explode('?', $signedUrl, 2); + [, $params] = explode('?', $signedUrl, 2); $signedUrl = $this->getUrl($path) . '?' . $params; } @@ -638,12 +622,12 @@ protected function basename(string $path): string // coverage is not reported. // Handle relative paths with drive letters. c:file.txt. - while (preg_match('#^[a-zA-Z]{1}:[^\\\/]#', $basename)) { + while (preg_match('#^[a-zA-Z]:[^\\\/]#', $basename)) { $basename = substr($basename, 2); } // Remove colon for standalone drive letter names. - if (preg_match('#^[a-zA-Z]{1}:$#', $basename)) { + if (preg_match('#^[a-zA-Z]:$#', $basename)) { $basename = rtrim($basename, ':'); } @@ -656,7 +640,7 @@ protected function basename(string $path): string */ protected function dirname(string $path): string { - return $this->normalizeDirname(dirname($path)); + return $this->normalizeDotName(dirname($path)); } /** @@ -674,7 +658,7 @@ protected function emulateDirectories(array $listing): array $directories = array_diff(array_unique($directories), array_unique($listedDirectories)); foreach ($directories as $directory) { - $listing[] = $this->pathinfo($directory) + ['type' => 'dir']; + $listing[] = $this->getPathInfo($directory) + ['type' => 'dir']; } return $listing; @@ -689,13 +673,13 @@ protected function emulateObjectDirectories(array $object, array $directories, a $listedDirectories[] = $object['path']; } - if ( ! isset($object['dirname']) || trim($object['dirname']) === '') { + if (!isset($object['dirname']) || trim($object['dirname']) === '') { return [$directories, $listedDirectories]; } $parent = $object['dirname']; - while (isset($parent) && trim($parent) !== '' && ! \in_array($parent, $directories, true)) { + while ($parent && trim($parent) !== '' && !\in_array($parent, $directories, true)) { $directories[] = $parent; $parent = $this->dirname($parent); } @@ -713,6 +697,7 @@ protected function getRawVisibility(string $path): string { try { $acl = $this->getObject($path)->acl()->get(['entity' => 'allUsers']); + return $acl['role'] === Acl::ROLE_READER ? Visibility::PUBLIC : Visibility::PRIVATE; } catch (NotFoundException $e) { // object may not have an acl entry, so handle that gracefully @@ -726,6 +711,7 @@ protected function getRawVisibility(string $path): string protected function getObject(string $path): StorageObject { $path = $this->applyPathPrefix($path); + return $this->bucket->object($path); } @@ -737,18 +723,30 @@ protected function getPredefinedAclForVisibility(string $visibility): string /** * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. */ - protected function pathinfo(string $path): array + protected function getPathInfo(string $path): array { $pathinfo = compact('path'); if ('' !== $dirname = dirname($path)) { - $pathinfo['dirname'] = $this->normalizeDirname($dirname); + $pathinfo['dirname'] = $this->normalizeDotName($dirname); } $pathinfo['basename'] = $this->basename($path); - $pathinfo += pathinfo($pathinfo['basename']); return $pathinfo + ['dirname' => '']; } + + /** + * Returns a normalised directory name from the given path. + */ + protected function normalizeDirPostfix(string $dirname): string + { + return rtrim($dirname, '/') . '/'; + } + + protected function normalizeDotName(string $dirname): string + { + return $dirname === '.' ? '' : $dirname; + } } diff --git a/tests/GoogleStorageAdapterTests.php b/tests/GoogleStorageAdapterTests.php index d76dc16..c726c5c 100755 --- a/tests/GoogleStorageAdapterTests.php +++ b/tests/GoogleStorageAdapterTests.php @@ -32,8 +32,15 @@ public function testGetBucket(): void $this->assertSame($bucket, $adapter->getBucket()); } - public function testWrite(): void - { + /** + * @dataProvider getDataForTestWriteContent + */ + public function testWriteContent( + array $expected, + string $contents, + string $predefinedAcl, + ?string $visibility + ): void { $bucket = $this->createMock(Bucket::class); $storageObject = $this->createMock(StorageObject::class); @@ -54,10 +61,10 @@ public function testWrite(): void ->expects($this->once()) ->method('upload') ->with( - 'This is the file contents.', + $contents, [ 'name' => 'prefix/file1.txt', - 'predefinedAcl' => 'projectPrivate', + 'predefinedAcl' => $predefinedAcl, ], ) ->willReturn($storageObject); @@ -72,122 +79,13 @@ public function testWrite(): void $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->write('file1.txt', 'This is the file contents.', new Config()); - - $expected = [ - 'type' => 'file', - 'dirname' => '', - 'path' => 'file1.txt', - 'timestamp' => 1474901082, - 'mimetype' => 'text/plain', - 'size' => 5, - ]; - $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); - } - - public function testWriteWithPrivateVisibility(): void - { - $bucket = $this->createMock(Bucket::class); - - $storageObject = $this->createMock(StorageObject::class); - $storageObject - ->expects($this->exactly(2)) - ->method('name') - ->willReturn('prefix/file1.txt'); - $storageObject - ->expects($this->exactly(2)) - ->method('info') - ->willReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket - ->expects($this->once()) - ->method('upload') - ->with( - 'This is the file contents.', - [ - 'name' => 'prefix/file1.txt', - 'predefinedAcl' => 'projectPrivate', - ] - ) - ->willReturn($storageObject); - - $bucket - ->expects($this->once()) - ->method('object') - ->with('prefix/file1.txt', []) - ->willReturn($storageObject); - - $storageClient = $this->createMock(StorageClient::class); - - $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - - $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => Visibility::PRIVATE])); - - $expected = [ - 'type' => 'file', - 'dirname' => '', - 'path' => 'file1.txt', - 'timestamp' => 1474901082, - 'mimetype' => 'text/plain', - 'size' => 5, - ]; - $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); - } - - public function testWriteWithPublicVisibility(): void - { - $bucket = $this->createMock(Bucket::class); - - $storageObject = $this->createMock(StorageObject::class); - $storageObject - ->expects($this->exactly(2)) - ->method('name') - ->willReturn('prefix/file1.txt'); - $storageObject - ->expects($this->exactly(2)) - ->method('info') - ->willReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket - ->expects($this->once()) - ->method('upload') - ->with( - 'This is the file contents.', - [ - 'name' => 'prefix/file1.txt', - 'predefinedAcl' => 'publicRead', - ], - ) - ->willReturn($storageObject); + $configOptions = []; + if ($visibility) { + $configOptions['visibility'] = $visibility; + } - $bucket - ->expects($this->once()) - ->method('object') - ->with('prefix/file1.txt', []) - ->willReturn($storageObject); - - $storageClient = $this->createMock(StorageClient::class); - - $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); + $adapter->write('file1.txt', 'This is the file contents.', new Config($configOptions)); - $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => Visibility::PUBLIC])); - - $expected = [ - 'type' => 'file', - 'dirname' => '', - 'path' => 'file1.txt', - 'timestamp' => 1474901082, - 'mimetype' => 'text/plain', - 'size' => 5, - ]; $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } @@ -465,7 +363,10 @@ public function testDelete(): void $adapter->delete('file.txt'); } - public function testDeleteDir(): void + /** + * @dataProvider getDataForTestDeleteDirectory + */ + public function testDeleteDirectory(string $path): void { $storageClient = $this->createMock(StorageClient::class); $bucket = $this->createMock(Bucket::class); @@ -497,56 +398,15 @@ public function testDeleteDir(): void ->expects($this->once()) ->method('objects') ->with([ - 'prefix' => 'prefix/dir_name/' + 'prefix' => 'prefix/dir_name/', ]) ->willReturn([$storageObject]); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->deleteDirectory('dir_name'); + $adapter->deleteDirectory($path); } - public function testDeleteDirWithTrailingSlash(): void - { - $storageClient = $this->createMock(StorageClient::class); - $bucket = $this->createMock(Bucket::class); - - $storageObject = $this->createMock(StorageObject::class); - $storageObject - ->expects($this->exactly(3)) - ->method('delete'); - - $storageObject - ->expects($this->once()) - ->method('name') - ->willReturn('prefix/dir_name/directory1/file1.txt', []); - $storageObject - ->expects($this->once()) - ->method('info') - ->willReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket - ->expects($this->exactly(3)) - ->method('object') - ->withConsecutive(['prefix/dir_name/directory1/file1.txt', []], ['prefix/dir_name/directory1/', []], ['prefix/dir_name/', []]) - ->willReturnOnConsecutiveCalls($storageObject, $storageObject, $storageObject); - - $bucket - ->expects($this->once()) - ->method('objects') - ->with([ - 'prefix' => 'prefix/dir_name/' - ]) - ->willReturn([$storageObject]); - - $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - - $adapter->deleteDirectory('dir_name//'); - } public function testSetVisibilityPrivate(): void { $bucket = $this->createMock(Bucket::class); @@ -1112,4 +972,27 @@ public function testGetUrl(): void // no bucket name on custom domain $this->assertEquals('http://my-domain.com/another-prefix/dir/file.txt', $adapter->getUrl('dir/file.txt')); } + + public function getDataForTestDeleteDirectory(): iterable + { + yield ['dir_name']; + yield ['dir_name//']; + } + + public function getDataForTestWriteContent(): iterable + { + $contents = 'This is the file contents.'; + $expected = [ + 'type' => 'file', + 'dirname' => '', + 'path' => 'file1.txt', + 'timestamp' => 1474901082, + 'mimetype' => 'text/plain', + 'size' => 5, + ]; + + yield [$expected, $contents, 'projectPrivate', null]; + yield [$expected, $contents, 'projectPrivate', Visibility::PRIVATE]; + yield [$expected, $contents, 'publicRead', Visibility::PUBLIC]; + } } From ccf16bde2e3f79e7508716898a666b8c95fa49f0 Mon Sep 17 00:00:00 2001 From: Anatoliy Melnikau Date: Thu, 22 Dec 2022 18:00:54 +0300 Subject: [PATCH 6/6] Update for the using of league/flysystem:^3 --- composer.json | 4 ++-- src/GoogleStorageAdapter.php | 11 +++++++++-- tests/GoogleStorageAdapterTests.php | 27 +++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index a7ff108..09318c5 100755 --- a/composer.json +++ b/composer.json @@ -9,9 +9,9 @@ } ], "require": { - "php": "^7.4", + "php": "^8.0", "google/cloud-storage": "~1.0", - "league/flysystem": "^2.0" + "league/flysystem": "^3.0" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/src/GoogleStorageAdapter.php b/src/GoogleStorageAdapter.php index 4a9a4ac..1be392d 100755 --- a/src/GoogleStorageAdapter.php +++ b/src/GoogleStorageAdapter.php @@ -51,6 +51,13 @@ public function applyPathPrefix(string $path): string return $this->getPathPrefix() . ltrim($path, '\\/'); } + public function directoryExists(string $path): bool + { + $object = $this->getObject($path); + + return str_ends_with($object->name(), '/') && $object->exists(); + } + public function fileExists(string $path): bool { return $this->getObject($path)->exists(); @@ -249,7 +256,7 @@ protected function normaliseObject(StorageObject $object): array $name = $this->removePathPrefix($object->name()); $info = $object->info(); - $isDir = substr($name, -1) === '/'; + $isDir = str_ends_with($name, '/'); if ($isDir) { $name = rtrim($name, '/'); } @@ -333,7 +340,7 @@ public function deleteDirectory(string $path): void $object['path'] = $this->normalizeDirPostfix($object['path']); } - if (strpos($object['path'], $path) !== false) { + if (str_contains($object['path'], $path)) { $filtered_objects[] = $object; } } diff --git a/tests/GoogleStorageAdapterTests.php b/tests/GoogleStorageAdapterTests.php index c726c5c..7c3f1c8 100755 --- a/tests/GoogleStorageAdapterTests.php +++ b/tests/GoogleStorageAdapterTests.php @@ -14,6 +14,33 @@ class GoogleStorageAdapterTests extends TestCase { + public function testDirectoryExists(): void + { + $storageClient = $this->createMock(StorageClient::class); + $bucket = $this->createMock(Bucket::class); + + $storageObject = $this->createMock(StorageObject::class); + $storageObject + ->expects($this->once()) + ->method('exists') + ->willReturn(true); + + $storageObject + ->expects($this->once()) + ->method('name') + ->willReturn('dir_name/'); + + $bucket + ->expects($this->once()) + ->method('object') + ->with('prefix/dir_name') + ->willReturn($storageObject); + + $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); + + self::assertTrue($adapter->directoryExists('dir_name')); + } + public function testGetStorageClient(): void { $storageClient = $this->createMock(StorageClient::class);