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..09318c5 100755 --- a/composer.json +++ b/composer.json @@ -9,25 +9,25 @@ } ], "require": { - "php": ">=5.5.0", - "league/flysystem": "~1.0", - "google/cloud-storage": "~1.0" + "php": "^8.0", + "google/cloud-storage": "~1.0", + "league/flysystem": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~4.0", - "mockery/mockery": "0.9.*" + "phpunit/phpunit": "^9.5", + "roave/security-advisories": "dev-latest" }, "autoload": { "psr-4": { "Superbalist\\Flysystem\\GoogleStorage\\": "src/" } }, - "config": { - "bin-dir": "bin" - }, - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" + "autoload-dev": { + "psr-4": { + "Superbalist\\Flysystem\\GoogleStorage\\Test\\": "tests/" } + }, + "config": { + "sort-packages": true } } 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..1be392d 100755 --- a/src/GoogleStorageAdapter.php +++ b/src/GoogleStorageAdapter.php @@ -8,123 +8,187 @@ 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 - */ - const STORAGE_API_URI_DEFAULT = 'https://storage.googleapis.com'; + public const STORAGE_API_URI_DEFAULT = 'https://storage.googleapis.com'; + + protected StorageClient $storageClient; + protected Bucket $bucket; + protected ?string $pathPrefix = null; + protected string $pathSeparator = '/'; + protected string $storageApiUri; + + public function __construct( + StorageClient $storageClient, + Bucket $bucket, + string $pathPrefix = null, + string $storageApiUri = null + ) { + $this->storageClient = $storageClient; + $this->bucket = $bucket; - /** - * @var StorageClient - */ - protected $storageClient; + if ($pathPrefix) { + $this->setPathPrefix($pathPrefix); + } + + $this->storageApiUri = ($storageApiUri) ?: self::STORAGE_API_URI_DEFAULT; + } /** - * @var Bucket + * 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. */ - protected $bucket; + 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(); + } + + 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'], + ], + ); + } /** - * @var string + * The method grabbed from class \League\Flysystem\Adapter\AbstractAdapter of league/flysystem:dev-1.0.x. + * It is public for the backward compatibility only. */ - protected $storageApiUri; + public function getPathPrefix(): ?string + { + return $this->pathPrefix; + } /** - * @param StorageClient $storageClient - * @param Bucket $bucket - * @param string $pathPrefix - * @param string $storageApiUri + * 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 __construct(StorageClient $storageClient, Bucket $bucket, $pathPrefix = null, $storageApiUri = null) + public function setPathPrefix(?string $prefix): void { - $this->storageClient = $storageClient; - $this->bucket = $bucket; + $prefix = (string) $prefix; - if ($pathPrefix) { - $this->setPathPrefix($pathPrefix); + if ($prefix === '') { + $this->pathPrefix = null; + + return; } - $this->storageApiUri = ($storageApiUri) ?: self::STORAGE_API_URI_DEFAULT; + $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; } + public function getStorageApiUri(): string + { + 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']); + } + /** - * Return the storage api uri. - * - * @return string + * 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 getStorageApiUri() + public function removePathPrefix(string $path): string { - return $this->storageApiUri; + 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, $contents, Config $config): void { - return $this->upload($path, $resource, $config); + $this->upload($path, $contents, $config); } /** - * {@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); } @@ -132,21 +196,15 @@ 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 = []; - - 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(AdapterInterface::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; @@ -158,13 +216,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); @@ -179,26 +242,31 @@ 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(); - $isDir = substr($name, -1) === '/'; + $isDir = str_ends_with($name, '/'); if ($isDir) { $name = rtrim($name, '/'); } 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'] : '', + 'mimetype' => $info['contentType'] ?? '', 'size' => $info['size'], ]; } @@ -206,55 +274,61 @@ 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 $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), ]; - $this->getObject($path)->copy($this->bucket, $options); - - return true; + if (!$this->getObject($source)->copy($this->bucket, $options)->exists()) { + throw UnableToCopyFile::fromLocationTo($source, $destination); + } } /** * {@inheritdoc} */ - public function delete($path) + public function delete(string $path): void { $this->getObject($path)->delete(); - - return true; } /** - * {@inheritdoc} + * @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); + + $this->deleteDirectory($dirname); + } + + 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; }); @@ -263,10 +337,10 @@ public function deleteDir($dirname) 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 (str_contains($object['path'], $path)) { $filtered_objects[] = $object; } } @@ -275,139 +349,183 @@ public function deleteDir($dirname) foreach ($filtered_objects as $object) { $this->delete($object['path']); } - - return true; } /** - * {@inheritdoc} + * @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 { - return $this->upload($this->normaliseDirName($dirname), '', $config); + @trigger_error(sprintf('Method "%s:createDir()" id deprecated. Use "%1$s:createDirectory()"', __CLASS__), \E_USER_DEPRECATED); + + return $this->upload($this->normalizeDirPostfix($dirname), '', $config); } /** - * Returns a normalised directory name from the given path. - * - * @param string $dirname - * - * @return string + * {@inheritdoc} */ - protected function normaliseDirName($dirname) + public function createDirectory(string $path, Config $config): void { - return rtrim($dirname, '/') . '/'; + $this->upload($this->normalizeDirPostfix($path), '', $config); } /** * {@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() } + * @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); + 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(); } /** * {@inheritdoc} */ - public function readStream($path) + public function readStream(string $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 $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) { $normalised[] = $this->normaliseObject($object); } - return Util::emulateDirectories($normalised); + return $this->emulateDirectories($normalised); } /** - * {@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); } /** - * {@inheritdoc} + * @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); + return $this->getMetadata($path); } /** - * {@inheritdoc} + * @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); + return $this->getMetadata($path); } /** - * {@inheritdoc} + * @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); + return $this->getMetadata($path); } /** - * {@inheritdoc} + * @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); + return [ 'visibility' => $this->getRawVisibility($path), ]; @@ -416,13 +534,9 @@ public function getVisibility($path) /** * Return a public url to a file. * - * Note: The file must have `AdapterInterface::VISIBILITY_PUBLIC` visibility. - * - * @param string $path - * - * @return string + * Note: The file must have `Visibility::PUBLIC` visibility. */ - public function getUrl($path) + public function getUrl(string $path): string { $uri = rtrim($this->storageApiUri, '/'); $path = $this->applyPathPrefix($path); @@ -444,7 +558,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. @@ -482,15 +596,14 @@ 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); if ($this->getStorageApiUri() !== self::STORAGE_API_URI_DEFAULT) { - list($url, $params) = explode('?', $signedUrl, 2); + [, $params] = explode('?', $signedUrl, 2); $signedUrl = $this->getUrl($path) . '?' . $params; } @@ -498,43 +611,149 @@ public function getTemporaryUrl($path, $expiration, $options = []) } /** - * @param string $path - * - * @return string + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. + */ + protected function basename(string $path): string + { + $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]:[^\\\/]#', $basename)) { + $basename = substr($basename, 2); + } + + // Remove colon for standalone drive letter names. + if (preg_match('#^[a-zA-Z]:$#', $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->normalizeDotName(dirname($path)); + } + + /** + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. */ - protected function getRawVisibility($path) + 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->getPathInfo($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 ($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]; + } + + protected function getRawVisibility(string $path): string { 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; } } /** * 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); } + protected function getPredefinedAclForVisibility(string $visibility): string + { + return $visibility === Visibility::PUBLIC ? 'publicRead' : 'projectPrivate'; + } + /** - * @param string $visibility - * - * @return string + * The method grabbed from class \League\Flysystem\Util of league/flysystem:dev-1.0.x. */ - protected function getPredefinedAclForVisibility($visibility) + protected function getPathInfo(string $path): array + { + $pathinfo = compact('path'); + + if ('' !== $dirname = dirname($path)) { + $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 $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'publicRead' : 'projectPrivate'; + return $dirname === '.' ? '' : $dirname; } } diff --git a/tests/GoogleStorageAdapterTests.php b/tests/GoogleStorageAdapterTests.php index 450dc51..7c3f1c8 100755 --- a/tests/GoogleStorageAdapterTests.php +++ b/tests/GoogleStorageAdapterTests.php @@ -1,157 +1,165 @@ 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); + $bucket = $this->createMock(Bucket::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket); $this->assertSame($storageClient, $adapter->getStorageClient()); } - public function testGetBucket() + public function testGetBucket(): void { - $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()); } - 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([ + /** + * @dataProvider getDataForTestWriteContent + */ + public function testWriteContent( + array $expected, + string $contents, + string $predefinedAcl, + ?string $visibility + ): 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->shouldReceive('upload') - ->withArgs([ - 'This is the file contents.', + $bucket + ->expects($this->once()) + ->method('upload') + ->with( + $contents, [ 'name' => 'prefix/file1.txt', - 'predefinedAcl' => 'projectPrivate', + 'predefinedAcl' => $predefinedAcl, ], - ]) - ->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()); + $configOptions = []; + if ($visibility) { + $configOptions['visibility'] = $visibility; + } - $expected = [ - 'type' => 'file', - 'dirname' => '', - 'path' => 'file1.txt', - 'timestamp' => 1474901082, - 'mimetype' => 'text/plain', - 'size' => 5, - ]; - $this->assertEquals($expected, $data); + $adapter->write('file1.txt', 'This is the file contents.', new Config($configOptions)); + + $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } - public function testWriteWithPrivateVisibility() + public function testWriteStream(): void { - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ + $stream = tmpfile(); + + $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([ - 'This is the file contents.', + $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); - $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - - $data = $adapter->write('file1.txt', 'This is the file contents.', new Config(['visibility' => AdapterInterface::VISIBILITY_PRIVATE])); - - $expected = [ - 'type' => 'file', - 'dirname' => '', - 'path' => 'file1.txt', - 'timestamp' => 1474901082, - 'mimetype' => 'text/plain', - 'size' => 5, - ]; - $this->assertEquals($expected, $data); - } - - 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([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, - ]); - - $bucket->shouldReceive('upload') - ->withArgs([ - 'This is the file contents.', - [ - 'name' => 'prefix/file1.txt', - 'predefinedAcl' => 'publicRead', - ], - ]) - ->once() - ->andReturn($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->writeStream('file1.txt', $stream, new Config()); + + fclose($stream); $expected = [ 'type' => 'file', @@ -161,467 +169,456 @@ public function testWriteWithPublicVisibility() 'mimetype' => 'text/plain', 'size' => 5, ]; - $this->assertEquals($expected, $data); + $this->assertEquals($expected, $adapter->getMetadata('file1.txt')); } - public function testWriteStream() + public function testRename(): void { - $stream = tmpfile(); + $bucket = $this->createMock(Bucket::class); - $bucket = Mockery::mock(Bucket::class); - - $storageObject = Mockery::mock(StorageObject::class); - $storageObject->shouldReceive('name') - ->once() - ->andReturn('prefix/file1.txt'); - $storageObject->shouldReceive('info') - ->once() - ->andReturn([ - 'updated' => '2016-09-26T14:44:42+00:00', - 'contentType' => 'text/plain', - 'size' => 5, + $oldStorageObjectAcl = $this->createMock(Acl::class); + $oldStorageObjectAcl + ->expects($this->once()) + ->method('get') + ->with(['entity' => 'allUsers']) + ->willReturn([ + 'role' => Acl::ROLE_OWNER, ]); - $bucket->shouldReceive('upload') - ->withArgs([ - $stream, + $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/file1.txt', + 'name' => 'prefix/new_file.txt', 'predefinedAcl' => 'projectPrivate', ], - ]) - ->once() - ->andReturn($storageObject); + ) + ->willReturn($newStorageObject); + $oldStorageObject + ->expects($this->exactly(0)) + ->method('delete'); + + $bucket + ->expects($this->exactly(2)) + ->method('object') + ->with('prefix/old_file.txt') + ->willReturn($oldStorageObject); - $storageClient = Mockery::mock(StorageClient::class); + $storageClient = $this->createMock(StorageClient::class); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $data = $adapter->writeStream('file1.txt', $stream, new Config()); - - fclose($stream); - - $expected = [ - 'type' => 'file', - 'dirname' => '', - 'path' => 'file1.txt', - 'timestamp' => 1474901082, - 'mimetype' => 'text/plain', - 'size' => 5, - ]; - $this->assertEquals($expected, $data); + $adapter->move('old_file.txt', 'new_file.txt', new Config()); } - public function testRename() + public function testDeleteOnRename(): void { - $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(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', ], - ]) - ->once(); - $oldStorageObject->shouldReceive('delete') - ->once(); - - $bucket->shouldReceive('object') + ) + ->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() + public function testCopy(): void { - $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() + public function testCopyWhenOriginalFileIsPublic(): void { - $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() + public function testDelete(): void { - $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'); $adapter->delete('file.txt'); } - 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([ - '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->shouldReceive('object') - ->with('prefix/dir_name/') - ->once() - ->andReturn($storageObject); - - $bucket->shouldReceive('objects') - ->with([ - 'prefix' => 'prefix/dir_name/' - ])->once() - ->andReturn([$storageObject]); - - $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - - $adapter->deleteDir('dir_name'); - } - - public function testDeleteDirWithTrailingSlash() + /** + * @dataProvider getDataForTestDeleteDirectory + */ + public function testDeleteDirectory(string $path): void { - $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]); + 'prefix' => 'prefix/dir_name/', + ]) + ->willReturn([$storageObject]); $adapter = new GoogleStorageAdapter($storageClient, $bucket, 'prefix'); - $adapter->deleteDir('dir_name//'); + $adapter->deleteDirectory($path); } - public function testSetVisibilityPrivate() + public function testSetVisibilityPrivate(): void { - $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() + public function testSetVisibilityPublic(): void { - $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() + public function testFileExists(): void { - $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() + public function testRead(): void { - $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() + public function testReadStream(): void { - $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() + public function testListContents(): void { - $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 +662,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, @@ -714,27 +715,30 @@ protected function getMockDirObjects($prefix = '') ]; } - public function testGetMetadataForFile() + public function testGetMetadataForFile(): void { - $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'); @@ -752,27 +756,30 @@ public function testGetMetadataForFile() $this->assertEquals($expected, $metadata); } - public function testGetMetadataForDir() + public function testGetMetadataForDir(): void { - $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'); @@ -790,27 +797,30 @@ public function testGetMetadataForDir() $this->assertEquals($expected, $metadata); } - public function testGetSize() + public function testGetSize(): void { - $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'); @@ -820,27 +830,30 @@ public function testGetSize() $this->assertEquals(5, $metadata['size']); } - public function testGetMimetype() + public function testGetMimetype(): void { - $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'); @@ -850,27 +863,30 @@ public function testGetMimetype() $this->assertEquals('text/plain', $metadata['mimetype']); } - public function testGetTimestamp() + public function testGetTimestamp(): void { - $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'); @@ -880,70 +896,76 @@ public function testGetTimestamp() $this->assertEquals(1474901082, $metadata['timestamp']); } - public function testGetVisibilityWhenVisibilityIsPrivate() + public function testGetVisibilityWhenVisibilityIsPrivate(): void { - $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() + public function testGetVisibilityWhenVisibilityIsPublic(): void { - $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() + public function testSetGetStorageApiUri(): void { - $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()); @@ -955,13 +977,15 @@ public function testSetGetStorageApiUri() $this->assertEquals('http://this.is.my.base.com', $adapter->getStorageApiUri()); } - public function testGetUrl() + public function testGetUrl(): void { - $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')); @@ -975,4 +999,27 @@ public function testGetUrl() // 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]; + } }