diff --git a/Readme.md b/Readme.md index e3143be..dd69237 100644 --- a/Readme.md +++ b/Readme.md @@ -31,9 +31,8 @@ $publicMountPoint = new \Cmp\Storage\MountPoint('/var/www/app/public', $fallBack $vfs->registerMountPoint($localMountPoint); $vfs->registerMountPoint($publicMountPoint); -/* + //move file from /tmp (FS) to /var/www/app/public (S3) and if fails try to move from /tmp (FS) to /var/www/app/public (FS) -*/ $vfs->move('/tmp/testfile.jpg','/var/www/app/public/avatar.jpg' ); ``` @@ -139,7 +138,7 @@ __Fluid calls:__ * `setStrategy(AbstractStorageCallStrategy $strategy)` : Set a custom strategy * `setLogger(LoggerInterface $logger)` : Set custom logger * `addAdapter($adapter)` : Add a new adapter -* `build(AbstractStorageCallStrategy $callStrategy = null, LoggerInterface $logger = null)` : Build the virtual storage +* `build(AbstractStorageCallStrategy $callStrategy = null)` : Build the virtual storage __Non fluid calls:__ diff --git a/src/Cmp/Storage/Adapter/FileSystemAdapter.php b/src/Cmp/Storage/Adapter/FileSystemAdapter.php index 62fbfd4..1b1b92c 100644 --- a/src/Cmp/Storage/Adapter/FileSystemAdapter.php +++ b/src/Cmp/Storage/Adapter/FileSystemAdapter.php @@ -2,21 +2,79 @@ namespace Cmp\Storage\Adapter; +use Cmp\Storage\AdapterInterface; use Cmp\Storage\Exception\FileExistsException; use Cmp\Storage\Exception\FileNotFoundException; use Cmp\Storage\Exception\InvalidPathException; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LogLevel; +use Psr\Log\NullLogger; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; /** * Class FileSystemAdapter. */ -class FileSystemAdapter implements \Cmp\Storage\AdapterInterface +class FileSystemAdapter implements AdapterInterface, LoggerAwareInterface { + use LoggerAwareTrait; + use LogicalChecksTrait; + /** * Adapter Name. */ const NAME = 'FileSystem'; const MAX_PATH_SIZE = 255; //The major part of fs has this limit + public function __construct() + { + $this->logger = new NullLogger(); + } + + /** + * Read a file. + * + * @param string $path The path to the file + * + * @throws FileNotFoundException + * + * @return string The file contents or false on failure + */ + public function get($path) + { + $path = $this->normalizePath($path); + $this->assertNotFileExists($path); + + return file_get_contents($path); + } + + private function normalizePath($path) + { + $this->assertFileMaxLength($path); + + return realpath($path); + } + + /** + * @param $path + * + * @throws InvalidPathException + */ + private function assertFileMaxLength($path) + { + if (strlen(basename($path)) > self::MAX_PATH_SIZE) { + $e = new InvalidPathException($path); + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Invalid path {path}.', + ['exception' => $e, 'path' => $path] + ); + + throw $e; + } + } + /** * Get Adapter name. * @@ -28,34 +86,36 @@ public function getName() } /** - * Check whether a file exists. - * - * @param string $path + * @param $path * - * @return bool + * @throws FileNotFoundException */ - public function exists($path) + private function assertNotFileExists($path) { - $path = $this->normalizePath($path); + if (!$this->exists($path) || !is_file($path)) { + $e = new FileNotFoundException($path); + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. File {path} not exists.', + ['exception' => $e, 'path' => $path] + ); - return file_exists($path); + throw $e; + } } /** - * Read a file. - * - * @param string $path The path to the file + * Check whether a file exists. * - * @throws \Cmp\Storage\FileNotFoundException + * @param string $path * - * @return string The file contents or false on failure + * @return bool */ - public function get($path) + public function exists($path) { $path = $this->normalizePath($path); - $this->assertNotFileExists($path); - return file_get_contents($path); + return file_exists($path); } /** @@ -63,7 +123,7 @@ public function get($path) * * @param string $path The path to the file * - * @throws \Cmp\Storage\FileNotFoundException + * @throws FileNotFoundException * * @return resource The path resource or false on failure */ @@ -78,8 +138,8 @@ public function getStream($path) /** * Rename a file. * - * @param string $path Path to the existing file - * @param string $newpath The new path of the file + * @param string $path Path to the existing file + * @param string $newpath The new path of the file * @param bool $overwrite * * @return bool Thrown if $newpath exists @@ -89,9 +149,7 @@ public function getStream($path) public function rename($path, $newpath, $overwrite = false) { $path = $this->normalizePath($path); - if (!$overwrite && $this->exists($newpath)) { - throw new FileExistsException($newpath); - } + $this->ensureWeCanWriteDestFile($newpath, $overwrite); $this->assertNotFileExists($path); return rename($path, $newpath); @@ -100,8 +158,8 @@ public function rename($path, $newpath, $overwrite = false) /** * Copy a file. * - * @param string $path Path to the existing file - * @param string $newpath The destination path of the copy + * @param string $path Path to the existing file + * @param string $newpath The destination path of the copy * * @return bool */ @@ -134,6 +192,28 @@ public function delete($path) } } + /** + * Removes directory recursively. + * + * @param string $path + * + * @return bool + */ + private function removeDirectory($path) + { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $fileinfo) { + $todo = ($fileinfo->isDir() ? 'rmdir' : 'unlink'); + $todo($fileinfo->getRealPath()); + } + + return rmdir($path); + } + /** * Create a file or update if exists. It will create the missing folders. * @@ -147,12 +227,8 @@ public function delete($path) public function put($path, $contents) { $this->assertFileMaxLength($path); - if (is_dir($path)) { - throw new InvalidPathException($path); - } - if (!$this->createParentFolder($path)) { - throw new InvalidPathException($path); - } + $this->assertIsDir($path); + $this->ensureParentPathExists($path); if (($size = file_put_contents($path, $contents)) === false) { return false; } @@ -161,52 +237,45 @@ public function put($path, $contents) } /** - * Create a file or update if exists. It will create the missing folders. - * - * @param string $path The path to the file - * @param resource $resource The file handle - * - * @return bool + * @param $path * * @throws InvalidPathException */ - public function putStream($path, $resource) + private function assertIsDir($path) { - $this->assertFileMaxLength($path); if (is_dir($path)) { - throw new InvalidPathException($path); - } - if (!$this->createParentFolder($path)) { - throw new InvalidPathException($path); - } - $stream = fopen($path, 'w+'); + $e = new InvalidPathException($path); - if (!$stream) { - return false; - } - - stream_copy_to_stream($resource, $stream); + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Path {path} is a directory.', + ['exception' => $e, 'path' => $path] + ); - return fclose($stream); + throw $e; + } } /** * @param $path * - * @throws FileNotFoundException + * @throws InvalidPathException */ - private function assertNotFileExists($path) + private function ensureParentPathExists($path) { - if (!$this->exists($path) || !is_file($path)) { - throw new FileNotFoundException($path); - } - } + if (!$this->createParentFolder($path)) { + $e = new InvalidPathException($path); - private function normalizePath($path) - { - $this->assertFileMaxLength($path); + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'. + $this->getName(). + '" fails. Parent path {path} is not ready and it\'s impossible to create it.', + ['exception' => $e, 'path' => $path] + ); - return realpath($path); + throw $e; + } } /** @@ -225,43 +294,29 @@ private function createParentFolder($path) } /** - * @param $path - * - * @throws InvalidPathException - */ - private function assertFileMaxLength($path) - { - if (strlen(basename($path)) > self::MAX_PATH_SIZE) { - throw new InvalidPathException($path); - } - } - - /** - * Removes directory recursively. + * Create a file or update if exists. It will create the missing folders. * - * @param string $path + * @param string $path The path to the file + * @param resource $resource The file handle * * @return bool + * + * @throws InvalidPathException */ - private function removeDirectory($path) + public function putStream($path, $resource) { - if (is_dir($path)) { - $objects = scandir($path); - foreach ($objects as $object) { - if ($object != '.' && $object != '..') { - if (is_dir($path.'/'.$object)) { - if (!$this->removeDirectory($path.'/'.$object)) { - return false; - } - } else { - if (!unlink($path.'/'.$object)) { - return false; - } - } - } - } - - return rmdir($path); + $this->assertFileMaxLength($path); + $this->assertIsDir($path); + $this->ensureParentPathExists($path); + + $stream = fopen($path, 'w+'); + + if (!$stream) { + return false; } + + stream_copy_to_stream($resource, $stream); + + return fclose($stream); } } diff --git a/src/Cmp/Storage/Adapter/LogicalChecksTrait.php b/src/Cmp/Storage/Adapter/LogicalChecksTrait.php new file mode 100644 index 0000000..9a2e7f8 --- /dev/null +++ b/src/Cmp/Storage/Adapter/LogicalChecksTrait.php @@ -0,0 +1,31 @@ +exists($newpath)) { + $e = new FileExistsException($newpath); + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Destination file {path} already exists.', + ['exception' => $e, 'path' => $newpath] + ); + + throw $e; + } + } + +} \ No newline at end of file diff --git a/src/Cmp/Storage/Adapter/S3AWSAdapter.php b/src/Cmp/Storage/Adapter/S3AWSAdapter.php index 2926f17..e46ca9d 100644 --- a/src/Cmp/Storage/Adapter/S3AWSAdapter.php +++ b/src/Cmp/Storage/Adapter/S3AWSAdapter.php @@ -2,24 +2,44 @@ namespace Cmp\Storage\Adapter; +use Aws\Result; use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; use Cmp\Storage\AdapterInterface; use Cmp\Storage\Exception\AdapterException; use Cmp\Storage\Exception\FileExistsException; +use Cmp\Storage\Exception\FileNotFoundException; +use Cmp\Storage\Exception\InvalidPathException; use Cmp\Storage\Exception\InvalidStorageAdapterException; +use InvalidArgumentException; +use Mimey\MimeTypes; +use Psr\Log\LoggerAwareInterface; +use Psr\Log\LoggerAwareTrait; +use Psr\Log\LogLevel; +use Psr\Log\NullLogger; /** * Class S3AWSAdapter. */ -class S3AWSAdapter implements AdapterInterface +class S3AWSAdapter implements AdapterInterface, LoggerAwareInterface { + use LoggerAwareTrait; + use LogicalChecksTrait; + const ACL_PUBLIC_READ = 'public-read'; /** * Adapter Name. */ const NAME = 'S3AWS'; - + /** + * @var array + */ + private static $mandatoryEnvVars = [ + 'AWS_REGION', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_BUCKET', + ]; /** * @var S3Client */ @@ -28,25 +48,23 @@ class S3AWSAdapter implements AdapterInterface * @var string */ private $bucket; - /** * @var $pathPrefix */ private $pathPrefix; - /** - * @var \Mimey\MimeTypes + * @var MimeTypes */ private $mimes; - /** * @var array */ - private static $mandatoryEnvVars = [ - 'AWS_REGION', - 'AWS_ACCESS_KEY_ID', - 'AWS_SECRET_ACCESS_KEY', - 'AWS_BUCKET', + private $config; + /** + * @var array + */ + private $options = [ + 'ACL' => self::ACL_PUBLIC_READ, ]; /** @@ -55,47 +73,57 @@ class S3AWSAdapter implements AdapterInterface * @param array $config * @param string $bucket * @param string $pathPrefix + * @param array $options * * @throws InvalidStorageAdapterException */ - public function __construct(array $config = [], $bucket = '', $pathPrefix = '') + public function __construct(array $config = [], $bucket = '', $pathPrefix = '', array $options = []) { $this->bucket = $bucket; + $this->logger = new NullLogger(); if (empty($config) || empty($bucket)) { $this->assertMandatoryConfigEnv(); - $config = $this->getConfigFromEnv(); + $config = $this->getConfigFromEnv(); $this->bucket = getenv('AWS_BUCKET'); } - $this->client = new S3Client($config); + $this->client = new S3Client($config); $this->pathPrefix = $pathPrefix; - $this->mimes = new \Mimey\MimeTypes(); + $this->mimes = new MimeTypes(); + $this->config = $config; + $this->options = array_merge($this->options, $options); } /** - * Get Adapter name. - * - * @return string + * @throws InvalidStorageAdapterException */ - public function getName() + private function assertMandatoryConfigEnv() { - return self::NAME; + foreach (self::$mandatoryEnvVars as $env) { + if (empty(getenv($env))) { + throw new InvalidStorageAdapterException( + 'The env "'. + $env. + '" is missing. Set it to run this adapter as builtin or use the regular constructor.' + ); + } + } } /** - * Check whether a file exists. - * - * @param string $path - * - * @return bool + * @return array */ - public function exists($path) + private function getConfigFromEnv() { - $path = $this->trimPrefix($path); - if ($this->client->doesObjectExist($this->bucket, $path)) { - return true; - } + $config = [ + 'version' => 'latest', + 'region' => getenv('AWS_REGION'), + 'credentials' => [ + 'key' => getenv('AWS_ACCESS_KEY_ID'), + 'secret' => getenv('AWS_SECRET_ACCESS_KEY'), + ], + ]; - return $this->doesDirectoryExist($path); + return $config; } /** @@ -118,24 +146,6 @@ public function get($path) return $response; } - /** - * Retrieves a read-stream for a path. - * - * @param string $path The path to the file - * - * @return resource The path resource or false on failure - */ - public function getStream($path) - { - $response = $this->readObject($path); - - if ($response !== false) { - $response = $response['Body']->detach(); - } - - return $response; - } - /** * @param $path * @@ -145,13 +155,13 @@ public function getStream($path) */ protected function readObject($path) { - $path = $this->trimPrefix($path); + $path = $this->trimPrefix($path); $command = $this->client->getCommand( 'getObject', [ 'Bucket' => $this->bucket, - 'Key' => $path, - '@http' => [ + 'Key' => $path, + '@http' => [ 'stream' => true, ], ] @@ -161,120 +171,95 @@ protected function readObject($path) /** @var Result $response */ $response = $this->client->execute($command); } catch (S3Exception $e) { + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Impossible read {path}.', + ['exception' => $e, 'path' => $path] + ); + return false; } return $response; } - /** - * Rename a file. - * - * @param string $path Path to the existing file - * @param string $newpath The new path of the file - * @param bool $overwrite - * - * @return bool Thrown if $newpath exists - * - * @throws FileExistsException - */ - public function rename($path, $newpath, $overwrite = false) + private function trimPrefix($prefix) { - if (!$overwrite && $this->exists($newpath)) { - throw new FileExistsException($newpath); - } - - if (!$this->copy($path, $newpath)) { - return false; - } + $from = '/'.preg_quote($this->pathPrefix, '/').'/'; + $prefix = preg_replace($from, '', $prefix, 1); - return $this->delete($path); + return ltrim($prefix, '/'); } /** - * Delete a file or directory. - * - * @param string $path + * Get Adapter name. * - * @return bool True on success, false on failure + * @return string */ - public function delete($path) + public function getName() { - $path = $this->trimPrefix($path); - try { - if ($this->doesDirectoryExist($path)) { - $this->client->deleteMatchingObjects($this->bucket, $path.'/'); - - return true; - } elseif ($this->client->doesObjectExist($this->bucket, $path)) { - $this->client->deleteMatchingObjects($this->bucket, $path); - - return true; - } - } catch (\Exception $e) { - return false; - } - - return false; + return self::NAME; } /** - * Create a file or update if exists. It will create the missing folders. - * - * @param string $path The path to the file - * @param string|resource $contents The file contents + * Retrieves a read-stream for a path. * - * @return bool True on success, false on failure + * @param string $path The path to the file * - * @throws \Cmp\Storage\InvalidPathException + * @return resource The path resource or false on failure */ - public function put($path, $contents) + public function getStream($path) { - $options = ['ContentType' => $this->getMimeType($path)]; - $path = $this->trimPrefix($path); - try { - $this->client->upload($this->bucket, $path, $contents, self::ACL_PUBLIC_READ, ['params' => $options]); - } catch (S3Exception $e) { - return false; + $response = $this->readObject($path); + + if ($response !== false) { + $response = $response['Body']->detach(); } - return true; + return $response; } /** - * Create a file or update if exists. It will create the missing folders. + * Rename a file. * - * @param string $path The path to the file - * @param resource $resource The file handle + * @param string $path Path to the existing file + * @param string $newpath The new path of the file + * @param bool $overwrite * - * @throws \Cmp\Storage\InvalidArgumentException Thrown if $resource is not a resource + * @return bool Thrown if $newpath exists * - * @return bool True on success, false on failure + * @throws FileExistsException */ - public function putStream($path, $resource) + public function rename($path, $newpath, $overwrite = false) { - return $this->put($path, $resource); + $this->ensureWeCanWriteDestFile($newpath, $overwrite); + + if (!$this->copy($path, $newpath)) { + return false; + } + + return $this->delete($path); } /** * Copy a file. * - * @param string $path Path to the existing file - * @param string $newpath The destination path of the copy + * @param string $path Path to the existing file + * @param string $newpath The destination path of the copy * * @return bool */ public function copy($path, $newpath) { - $path = $this->trimPrefix($path); + $path = $this->trimPrefix($path); $newpath = $this->trimPrefix($newpath); $command = $this->client->getCommand( 'copyObject', [ - 'Bucket' => $this->bucket, - 'Key' => $newpath, - 'CopySource' => urlencode($this->bucket.'/'.$path), - 'ACL' => self::ACL_PUBLIC_READ, + 'Bucket' => $this->bucket, + 'Key' => $newpath, + 'CopySource' => urlencode($this->bucket.'/'.$path), + 'ACL' => $this->options['ACL'], 'ContentType' => $this->getMimeType($path), ] ); @@ -282,12 +267,61 @@ public function copy($path, $newpath) try { $this->client->execute($command); } catch (S3Exception $e) { + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Impossible copy file from {from}} to {to}.', + ['exception' => $e, 'from' => $path, 'to' => $newpath] + ); + return false; } return true; } + private function getMimeType($path) + { + $ext = pathinfo($path, PATHINFO_EXTENSION); + if (empty($ext)) { + return 'text/plain'; + } + + return $this->mimes->getMimeType($ext); + } + + /** + * Delete a file or directory. + * + * @param string $path + * + * @return bool True on success, false on failure + */ + public function delete($path) + { + $path = $this->trimPrefix($path); + try { + if ($this->doesDirectoryExist($path)) { + $this->client->deleteMatchingObjects($this->bucket, $path.'/'); + + return true; + } elseif ($this->client->doesObjectExist($this->bucket, $path)) { + $this->client->deleteMatchingObjects($this->bucket, $path); + + return true; + } + } catch (\Exception $e) { + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Impossible delete file {path}.', + ['exception' => $e, 'path' => $path] + ); + + return false; + } + + return false; + } + /** * @param $path * @@ -300,8 +334,8 @@ private function doesDirectoryExist($path) $command = $this->client->getCommand( 'listObjects', [ - 'Bucket' => $this->bucket, - 'Prefix' => $this->trimPrefix($path).'/', + 'Bucket' => $this->bucket, + 'Prefix' => $this->trimPrefix($path).'/', 'MaxKeys' => 1, ] ); @@ -311,56 +345,74 @@ private function doesDirectoryExist($path) return $result['Contents'] || $result['CommonPrefixes']; } catch (S3Exception $e) { + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Impossible get information about directory {path}.', + ['exception' => $e, 'from' => $path] + ); + return false; } } /** - * @throws InvalidStorageAdapterException + * Check whether a file exists. + * + * @param string $path + * + * @return bool */ - private function assertMandatoryConfigEnv() + public function exists($path) { - foreach (self::$mandatoryEnvVars as $env) { - if (empty(getenv($env))) { - throw new InvalidStorageAdapterException( - 'The env "'. - $env. - '" is missing. Set it to run this adapter as builtin or use the regular constructor.' - ); - } + $path = $this->trimPrefix($path); + if ($this->client->doesObjectExist($this->bucket, $path)) { + return true; } + + return $this->doesDirectoryExist($path); } /** - * @return array + * Create a file or update if exists. It will create the missing folders. + * + * @param string $path The path to the file + * @param resource $resource The file handle + * + * @throws InvalidArgumentException Thrown if $resource is not a resource + * + * @return bool True on success, false on failure */ - private function getConfigFromEnv() + public function putStream($path, $resource) { - $config = [ - 'version' => 'latest', - 'region' => getenv('AWS_REGION'), - 'credentials' => [ - 'key' => getenv('AWS_ACCESS_KEY_ID'), - 'secret' => getenv('AWS_SECRET_ACCESS_KEY'), - ], - ]; - - return $config; + return $this->put($path, $resource); } - private function trimPrefix($prefix) + /** + * Create a file or update if exists. It will create the missing folders. + * + * @param string $path The path to the file + * @param string|resource $contents The file contents + * + * @return bool True on success, false on failure + * + * @throws InvalidPathException + */ + public function put($path, $contents) { - $from = '/'.preg_quote($this->pathPrefix, '/').'/'; - $prefix = preg_replace($from, '', $prefix, 1); - return ltrim($prefix, '/'); - } + $options = ['ContentType' => $this->getMimeType($path)]; + $path = $this->trimPrefix($path); + try { + $this->client->upload($this->bucket, $path, $contents, $this->options['ACL'], ['params' => $options]); + } catch (S3Exception $e) { + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$this->getName().'" fails. Impossible upload a file {path}.', + ['exception' => $e, 'path' => $path] + ); - private function getMimeType($path) - { - $ext = pathinfo($path, PATHINFO_EXTENSION); - if (empty($ext)) { - return 'text/plain'; + return false; } - return $this->mimes->getMimeType($ext); + + return true; } } diff --git a/src/Cmp/Storage/Exception/InvalidStorageAdapterException.php b/src/Cmp/Storage/Exception/InvalidStorageAdapterException.php index 957cace..054b212 100644 --- a/src/Cmp/Storage/Exception/InvalidStorageAdapterException.php +++ b/src/Cmp/Storage/Exception/InvalidStorageAdapterException.php @@ -10,6 +10,7 @@ class InvalidStorageAdapterException extends StorageException { const CODE = 1004; + /** * InvalidStorageAdapterException constructor. * diff --git a/src/Cmp/Storage/Exception/StorageAdapterNotFoundException.php b/src/Cmp/Storage/Exception/StorageAdapterNotFoundException.php deleted file mode 100644 index d5d0dd2..0000000 --- a/src/Cmp/Storage/Exception/StorageAdapterNotFoundException.php +++ /dev/null @@ -1,24 +0,0 @@ -virtualPath = new VirtualPath($path); + $this->virtualPath = new VirtualPath($path); $this->virtualStorage = $virtualStorage; } diff --git a/src/Cmp/Storage/MountPointSortedSet.php b/src/Cmp/Storage/MountPointSortedSet.php index e4912bb..ffa99cb 100644 --- a/src/Cmp/Storage/MountPointSortedSet.php +++ b/src/Cmp/Storage/MountPointSortedSet.php @@ -34,13 +34,11 @@ public function set(MountPoint $value) } /** - * @param $path - * * @return bool */ - public function contains($path) + private function sort() { - return isset($this->mountPoints[$path]); + return uksort($this->mountPoints, [$this, 'compare']); } /** @@ -57,6 +55,16 @@ public function get($path) return $this->mountPoints[$path]; } + /** + * @param $path + * + * @return bool + */ + public function contains($path) + { + return isset($this->mountPoints[$path]); + } + /** * @param $path * @@ -73,6 +81,21 @@ public function remove($path) return true; } + /** + * Retrieve an external iterator. + * + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * + * @return Traversable An instance of an object implementing Iterator or + * Traversable + * + * @since 5.0.0 + */ + public function getIterator() + { + return new ArrayIterator($this->mountPoints); + } + /** * @param string $value1 * @param string $value2 @@ -90,27 +113,4 @@ private function compare($value1, $value2) return $s2 - $s1; } - - /** - * @return bool - */ - private function sort() - { - return uksort($this->mountPoints, [$this, 'compare']); - } - - /** - * Retrieve an external iterator. - * - * @link http://php.net/manual/en/iteratoraggregate.getiterator.php - * - * @return Traversable An instance of an object implementing Iterator or - * Traversable - * - * @since 5.0.0 - */ - public function getIterator() - { - return new ArrayIterator($this->mountPoints); - } } diff --git a/src/Cmp/Storage/MountableVirtualStorage.php b/src/Cmp/Storage/MountableVirtualStorage.php index b827e78..f7428ad 100644 --- a/src/Cmp/Storage/MountableVirtualStorage.php +++ b/src/Cmp/Storage/MountableVirtualStorage.php @@ -23,14 +23,21 @@ class MountableVirtualStorage implements VirtualStorageInterface */ public function __construct($defaultVirtualStorage) { - $this->mountPoints = new MountPointSortedSet(); + $this->mountPoints = new MountPointSortedSet(); $this->defaultMountPoint = $this->getDefaultMountPoint($defaultVirtualStorage); $this->registerMountPoint($this->defaultMountPoint); } - public function getMountPoints() + /** + * @param $defaultVirtualStorage + * + * @return MountPoint + */ + private function getDefaultMountPoint(VirtualStorageInterface $defaultVirtualStorage) { - return $this->mountPoints->getIterator(); + $defaultMountPoint = new MountPoint(self::ROOT_PATH, $defaultVirtualStorage); + + return $defaultMountPoint; } /** @@ -41,6 +48,30 @@ public function registerMountPoint(MountPoint $mountPoint) $this->mountPoints->set($mountPoint); } + public function getMountPoints() + { + return $this->mountPoints->getIterator(); + } + + public function exists($path) + { + $vp = new VirtualPath($path); + + return $this->getStorageForPath($vp)->exists($vp->getPath()); + } + + /** + * @param VirtualPath $vp + * + * @return VirtualStorageInterface + */ + private function getStorageForPath(VirtualPath $vp) + { + $mountPoint = $this->getMountPointForPath($vp); + + return $mountPoint->getStorage(); + } + /** * @param VirtualPath $vp * @@ -60,44 +91,38 @@ public function getMountPointForPath(VirtualPath $vp) return $this->defaultMountPoint; } - public function exists($path) - { - $vp = new VirtualPath($path); - return $this->getStorageForPath($vp)->exists($vp->getPath()); - } - public function get($path) { $vp = new VirtualPath($path); + return $this->getStorageForPath($vp)->get($vp->getPath()); } public function getStream($path) { $vp = new VirtualPath($path); + return $this->getStorageForPath($vp)->getStream($vp->getPath()); } public function rename($path, $newpath, $overwrite = false) { - $svp = new VirtualPath($path); - $dvp = new VirtualPath($newpath); + $svp = new VirtualPath($path); + $dvp = new VirtualPath($newpath); $storageSrc = $this->getStorageForPath($svp); $storageDst = $this->getStorageForPath($dvp); if (!$overwrite && $storageDst->exists($dvp->getPath())) { throw new FileExistsException($dvp->getPath()); - } return $this->copy($svp->getPath(), $dvp->getPath()) && $storageSrc->delete($svp->getPath()); } - public function copy($path, $newpath) { - $svp = new VirtualPath($path); - $dvp = new VirtualPath($newpath); + $svp = new VirtualPath($path); + $dvp = new VirtualPath($newpath); $storageSrc = $this->getStorageForPath($svp); $storageDst = $this->getStorageForPath($dvp); @@ -114,47 +139,24 @@ public function copy($path, $newpath) return $storageDst->exists($dvp->getPath()); } - public function delete($path) { $vp = new VirtualPath($path); + return $this->getStorageForPath($vp)->delete($vp->getPath()); } public function put($path, $contents) { $vp = new VirtualPath($path); + return $this->getStorageForPath($vp)->put($vp->getPath(), $contents); } public function putStream($path, $resource) { $vp = new VirtualPath($path); - return $this->getStorageForPath($vp)->putStream($vp->getPath(), $resource); - } - /** - * @param $defaultVirtualStorage - * - * @return MountPoint - */ - private function getDefaultMountPoint(VirtualStorageInterface $defaultVirtualStorage) - { - $defaultMountPoint = new MountPoint(self::ROOT_PATH, $defaultVirtualStorage); - - return $defaultMountPoint; - } - - - /** - * @param VirtualPath $vp - * - * @return VirtualStorageInterface - */ - private function getStorageForPath(VirtualPath $vp) - { - $mountPoint = $this->getMountPointForPath($vp); - - return $mountPoint->getStorage(); + return $this->getStorageForPath($vp)->putStream($vp->getPath(), $resource); } } diff --git a/src/Cmp/Storage/StorageBuilder.php b/src/Cmp/Storage/StorageBuilder.php index e60f819..61335c3 100644 --- a/src/Cmp/Storage/StorageBuilder.php +++ b/src/Cmp/Storage/StorageBuilder.php @@ -4,12 +4,12 @@ use Cmp\Storage\Adapter\FileSystemAdapter; use Cmp\Storage\Exception\InvalidStorageAdapterException; -use Cmp\Storage\Exception\StorageAdapterNotFoundException; use Cmp\Storage\Strategy\AbstractStorageCallStrategy; use Cmp\Storage\Strategy\DefaultStrategyFactory; use Psr\Log\LoggerAwareInterface; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Psr\Log\NullLogger; /** * Class StorageBuilder. @@ -28,14 +28,6 @@ class StorageBuilder implements LoggerAwareInterface * @var array */ private $adapters; - /** - * @var array - */ - private static $builtinAdapters = []; - /** - * @var bool - */ - private static $builtInAdaptersLoaded = false; /** * StorageBuilder constructor. @@ -43,35 +35,41 @@ class StorageBuilder implements LoggerAwareInterface public function __construct() { $this->adapters = []; + $this->logger = new NullLogger(); } /** - * Set a custom strategy. + * Build the virtual storage. * - * @param AbstractStorageCallStrategy $strategy + * @param $callStrategy * - * @return $this + * @return VirtualStorageInterface + * + * @throws InvalidStorageAdapterException */ - public function setStrategy(AbstractStorageCallStrategy $strategy) + public function build(AbstractStorageCallStrategy $callStrategy = null) { - $this->log(LogLevel::INFO, 'Set the strategy {{strategy}}', ['strategy' => $strategy->getStrategyName()]); - $this->strategy = $strategy; + if (!$this->hasLoadedAdapters()) { + $this->addAdapter($this->getDefaultBuiltinAdapter()); + } - return $this; + if ($callStrategy != null) { + $this->setStrategy($callStrategy); + } + + $this->bindAdaptersLogger(); + + return $this->createStrategy(); } /** - * Set custom logger. - * - * @param LoggerInterface $logger + * Check if one or more adapters has been loaded. * - * @return $this + * @return bool */ - public function setLogger(LoggerInterface $logger) + public function hasLoadedAdapters() { - $this->logger = $logger; - - return $this; + return !empty($this->adapters); } /** @@ -82,51 +80,56 @@ public function setLogger(LoggerInterface $logger) * @return $this * * @throws InvalidStorageAdapterException - * @throws StorageAdapterNotFoundException */ public function addAdapter($adapter) { - if (is_string($adapter)) { - $this->addBuiltinAdapters(); - $this->assertBuiltInAdapterExists($adapter); - $this->registerAdapter(self::$builtinAdapters[$adapter]); - - return $this; - } - if ($adapter instanceof AdapterInterface) { $this->registerAdapter($adapter); return $this; } - throw new InvalidStorageAdapterException('Invalid storage adapter: '.get_class($adapter)); + throw new InvalidStorageAdapterException('Invalid storage adapter.'); } /** - * Build the virtual storage. - * - * @param $callStrategy - * @param LoggerInterface $logger - * - * @return VirtualStorageInterface - * - * @throws InvalidStorageAdapterException + * @param AdapterInterface $adapter */ - public function build(AbstractStorageCallStrategy $callStrategy = null, LoggerInterface $logger = null) + private function registerAdapter(AdapterInterface $adapter) { - if (!$this->hasLoadedAdapters()) { - $this->addAdapter($this->getDefaultBuiltinAdapter()); - } + $this->adapters[] = $adapter; + $this->logger->log(LogLevel::INFO, 'Added new adapter {adapter}', ['adapter' => $adapter->getName()]); + } - if ($callStrategy != null) { - $this->setStrategy($callStrategy); - } - if ($logger != null) { - $this->setLogger($logger); + private function getDefaultBuiltinAdapter() + { + return new FileSystemAdapter(); + } + + private function bindAdaptersLogger() + { + foreach ($this->adapters as $adapter) { + if ($adapter instanceof LoggerAwareInterface) { + $adapter->setLogger($this->logger); + } } + } - return $this->createStrategy(); + /** + * @return AbstractStorageCallStrategy + */ + private function createStrategy() + { + $strategy = $this->getStrategy(); + $strategy->setLogger($this->logger); + $strategy->setAdapters($this->adapters); + $this->logger->log( + LogLevel::INFO, + 'Creating strategy {strategy}', + ['strategy' => $strategy->getStrategyName()] + ); + + return $strategy; } /** @@ -144,54 +147,20 @@ public function getStrategy() } /** - * Get the current Logger. + * Set a custom strategy. * - * @return LoggerInterface - */ - public function getLogger() - { - return $this->logger; - } - - /** - * Check if one or more adapters has been loaded. + * @param AbstractStorageCallStrategy $strategy * - * @return bool - */ - public function hasLoadedAdapters() - { - return !empty($this->adapters); - } - - /** - * @param AdapterInterface $adapter - */ - private function registerAdapter(AdapterInterface $adapter) - { - if ($this->logger && $adapter instanceof LoggerAwareInterface) { - $adapter->setLogger($this->logger); - } - $this->adapters[] = $adapter; - $this->log(LogLevel::INFO, 'Added new adapter {{adapter}}', ['adapter' => $adapter->getName()]); - } - - /** * @return $this */ - private function addBuiltinAdapters() + public function setStrategy(AbstractStorageCallStrategy $strategy) { - if (!self::$builtInAdaptersLoaded) { - self::$builtInAdaptersLoaded = true; - foreach (glob(__DIR__.DIRECTORY_SEPARATOR.'Adapter'.DIRECTORY_SEPARATOR.'*.php') as $adapterFileName) { - $className = __NAMESPACE__.'\\'.'Adapter'.'\\'.basename($adapterFileName, '.php'); - try { - $class = new $className(); - self::$builtinAdapters[$class->getName()] = $class; - } catch (\Exception $e) { - $this->log(LogLevel::INFO, 'Impossible start {{className}} client', ['className' => $className]); - } - } - } + $this->logger->log( + LogLevel::INFO, + 'Set the strategy {strategy}', + ['strategy' => $strategy->getStrategyName()] + ); + $this->strategy = $strategy; return $this; } @@ -204,43 +173,27 @@ private function getDefaultCallStrategy() return DefaultStrategyFactory::create(); } - private function getDefaultBuiltinAdapter() - { - return FileSystemAdapter::NAME; - } - /** - * @param $adapter + * Get the current Logger. * - * @throws StorageAdapterNotFoundException + * @return LoggerInterface */ - private function assertBuiltInAdapterExists($adapter) - { - if (!array_key_exists($adapter, self::$builtinAdapters)) { - throw new StorageAdapterNotFoundException("Builtin storage \"$adapter\" not found"); - } - } - - private function log($level, $msg, $context) + public function getLogger() { - if (!$this->getLogger()) { - return; - } - $this->getLogger()->log($level, $msg, $context); + return $this->logger; } /** - * @return AbstractStorageCallStrategy + * Set custom logger. + * + * @param LoggerInterface $logger + * + * @return $this */ - private function createStrategy() + public function setLogger(LoggerInterface $logger) { - $strategy = $this->getStrategy(); - $strategy->setAdapters($this->adapters); - if ($this->getLogger()) { - $strategy->setLogger($this->getLogger()); - } - $this->log(LogLevel::INFO, 'Creating strategy {{strategy}}', ['strategy' => $strategy->getStrategyName()]); + $this->logger = $logger; - return $strategy; + return $this; } } diff --git a/src/Cmp/Storage/Strategy/AbstractStorageCallStrategy.php b/src/Cmp/Storage/Strategy/AbstractStorageCallStrategy.php index 54d0cfc..da5da0b 100644 --- a/src/Cmp/Storage/Strategy/AbstractStorageCallStrategy.php +++ b/src/Cmp/Storage/Strategy/AbstractStorageCallStrategy.php @@ -5,41 +5,23 @@ use Cmp\Storage\AdapterInterface; use Cmp\Storage\VirtualStorageInterface; use Psr\Log\LoggerAwareInterface; -use Psr\Log\LoggerInterface; +use Psr\Log\LoggerAwareTrait; use Psr\Log\LogLevel; +use Psr\Log\NullLogger; /** * Class AbstractStorageCallStrategy. */ abstract class AbstractStorageCallStrategy implements VirtualStorageInterface, LoggerAwareInterface { - /** - * @var LoggerInterface - */ - private $logger; + use LoggerAwareTrait; + private $adapters; public function __construct() { $this->adapters = []; - } - - public function addAdapter(AdapterInterface $adapter) - { - $this->log( - LogLevel::INFO, - 'Add adapter "{{adapter}}" to strategy "{{strategy}}"', - ['adapter' => $adapter->getName(), 'strategy' => $this->getStrategyName()] - ); - $this->adapters[] = $adapter; - } - - public function setAdapters(array $adapters) - { - $this->adapters = []; - foreach ($adapters as $adapter) { - $this->addAdapter($adapter); - } + $this->logger = new NullLogger(); } /** @@ -50,27 +32,22 @@ public function getAdapters() return $this->adapters; } - /** - * @param LoggerInterface $logger - */ - public function setLogger(LoggerInterface $logger) + public function setAdapters(array $adapters) { - $this->logger = $logger; + $this->adapters = []; + foreach ($adapters as $adapter) { + $this->addAdapter($adapter); + } } - /** - * Logs with an arbitrary level. - * - * @param mixed $level - * @param string $message - * @param array $context - */ - public function log($level, $message, array $context = array()) + public function addAdapter(AdapterInterface $adapter) { - if (!$this->logger) { - return; - } - $this->logger->log($level, $message, $context); + $this->logger->log( + LogLevel::INFO, + 'Add adapter "{adapter}" to strategy "{strategy}".', + ['adapter' => $adapter->getName(), 'strategy' => $this->getStrategyName()] + ); + $this->adapters[] = $adapter; } abstract public function getStrategyName(); diff --git a/src/Cmp/Storage/Strategy/CallAllStrategy.php b/src/Cmp/Storage/Strategy/CallAllStrategy.php index e6673db..4766f06 100644 --- a/src/Cmp/Storage/Strategy/CallAllStrategy.php +++ b/src/Cmp/Storage/Strategy/CallAllStrategy.php @@ -2,16 +2,15 @@ namespace Cmp\Storage\Strategy; -use Cmp\Storage\Exception\AdapterException; -use Cmp\Storage\Exception\FileNotFoundException; -use Cmp\Storage\Exception\StorageException; -use Psr\Log\LogLevel; +use Cmp\Storage\AdapterInterface; /** * Class CallAllStrategy. */ class CallAllStrategy extends AbstractStorageCallStrategy { + use RunAndLogTrait; + public function getStrategyName() { return 'CallAllStrategy'; @@ -26,11 +25,11 @@ public function getStrategyName() */ public function exists($path) { - $fn = function ($adapter) use ($path) { + $fn = function (AdapterInterface $adapter) use ($path) { return $adapter->exists($path); }; - return $this->runAllAndLog($fn); + return $this->runAll($fn); } /** @@ -44,11 +43,11 @@ public function exists($path) */ public function get($path) { - $fn = function ($adapter) use ($path) { + $fn = function (AdapterInterface $adapter) use ($path) { return $adapter->get($path); }; - return $this->runOneAndLog($fn, $path); + return $this->logOnFalse($this->runOne($fn, $path), "Impossible get file: {file}.", ['file' => $path]); } /** @@ -62,11 +61,15 @@ public function get($path) */ public function getStream($path) { - $fn = function ($adapter) use ($path) { + $fn = function (AdapterInterface $adapter) use ($path) { return $adapter->getStream($path); }; - return $this->runOneAndLog($fn, $path); + return $this->logOnFalse( + $this->runOne($fn, $path), + "Impossible get stream form file: {file}.", + ['file' => $path] + ); } /** @@ -80,28 +83,36 @@ public function getStream($path) */ public function rename($path, $newpath, $overwrite = false) { - $fn = function ($adapter) use ($path, $newpath, $overwrite) { + $fn = function (AdapterInterface $adapter) use ($path, $newpath, $overwrite) { return $adapter->rename($path, $newpath, $overwrite); }; - return $this->runAllAndLog($fn); + return $this->logOnFalse( + $this->runAll($fn), + "Impossible rename file from {from} to {to}.", + ['from' => $path, 'to' => $newpath] + ); } /** * Copy a file. * - * @param string $path Path to the existing file - * @param string $newpath The new path of the file + * @param string $path Path to the existing file + * @param string $newpath The new path of the file * * @return bool */ public function copy($path, $newpath) { - $fn = function ($adapter) use ($path, $newpath) { + $fn = function (AdapterInterface $adapter) use ($path, $newpath) { return $adapter->copy($path, $newpath); }; - return $this->runAllAndLog($fn); + return $this->logOnFalse( + $this->runAll($fn), + "Impossible copy file from {from} to {to}.", + ['from' => $path, 'to' => $newpath] + ); } /** @@ -115,11 +126,11 @@ public function copy($path, $newpath) */ public function delete($path) { - $fn = function ($adapter) use ($path) { + $fn = function (AdapterInterface $adapter) use ($path) { return $adapter->delete($path); }; - return $this->runAllAndLog($fn); + return $this->logOnFalse($this->runAll($fn), "Impossible delete file {file}.", ['file' => $path]); } /** @@ -134,11 +145,11 @@ public function delete($path) */ public function put($path, $contents) { - $fn = function ($adapter) use ($path, $contents) { + $fn = function (AdapterInterface $adapter) use ($path, $contents) { return $adapter->put($path, $contents); }; - return $this->runAllAndLog($fn); + return $this->logOnFalse($this->runAll($fn), "Impossible put file {file}.", ['file' => $path]); } /** @@ -147,77 +158,16 @@ public function put($path, $contents) * @param string $path The path to the file * @param resource $resource The file handle * - * @throws \Cmp\Storage\Exception\InvalidArgumentException Thrown if $resource is not a resource + * @throws \InvalidArgumentException Thrown if $resource is not a resource * * @return bool True on success, false on failure */ public function putStream($path, $resource) { - $fn = function ($adapter) use ($path, $resource) { + $fn = function (AdapterInterface $adapter) use ($path, $resource) { return $adapter->putStream($path, $resource); }; - return $this->runAllAndLog($fn); - } - - /** - * @param callable $fn - * - * @return bool - */ - private function runAllAndLog(callable $fn) - { - $result = false; - - foreach ($this->getAdapters() as $adapter) { - try { - $result = $fn($adapter) || $result; - } catch (\Exception $e) { - $this->logAdapterException($adapter, $e); - } - } - - return $result; - } - - /** - * Gets one file from all the adapters. - * - * @param callable $fn - * @param string $path - * - * @return mixed - * - * @throws StorageException - */ - private function runOneAndLog(callable $fn, $path) - { - $defaultException = new FileNotFoundException($path); - foreach ($this->getAdapters() as $adapter) { - try { - $file = $fn($adapter); - if ($file !== false) { - return $file; - } - } catch (\Exception $exception) { - $defaultException = new AdapterException($path, $exception); - $this->logAdapterException($adapter, $exception); - } - } - - throw $defaultException; - } - - /** - * @param \Cmp\Storage\AdapterInterface $adapter - * @param \Exception $e - */ - private function logAdapterException($adapter, $e) - { - $this->log( - LogLevel::ERROR, - 'Adapter "'.$adapter->getName().'" fails.', - ['exception' => $e] - ); + return $this->logOnFalse($this->runAll($fn), "Impossible put file stream {file}.", ['file' => $path]); } } diff --git a/src/Cmp/Storage/Strategy/FallBackChainStrategy.php b/src/Cmp/Storage/Strategy/FallBackChainStrategy.php index 02a7bca..13d0738 100644 --- a/src/Cmp/Storage/Strategy/FallBackChainStrategy.php +++ b/src/Cmp/Storage/Strategy/FallBackChainStrategy.php @@ -5,13 +5,14 @@ use Cmp\Storage\AdapterInterface; use Cmp\Storage\Exception\FileExistsException; use Cmp\Storage\Exception\FileNotFoundException; -use Psr\Log\LogLevel; /** * Class FallBackChainStrategy. */ class FallBackChainStrategy extends AbstractStorageCallStrategy { + use RunAndLogTrait; + public function getStrategyName() { return 'FallBackChainStrategy'; @@ -28,7 +29,7 @@ public function exists($path) return $adapter->exists($path); }; - return $this->runChainAndLog($fn); + return $this->runChain($fn); } /** @@ -46,7 +47,7 @@ public function get($path) return $adapter->get($path); }; - return $this->runChainAndLog($fn); + return $this->logOnFalse($this->runChain($fn), "Impossible get file: {file}.", ['file' => $path]); } /** @@ -64,14 +65,18 @@ public function getStream($path) return $adapter->getStream($path); }; - return $this->runChainAndLog($fn); + return $this->logOnFalse( + $this->runChain($fn), + "Impossible get stream form file: {file}.", + ['file' => $path] + ); } /** * Rename a file. * - * @param string $path Path to the existing file - * @param string $newpath The new path of the file + * @param string $path Path to the existing file + * @param string $newpath The new path of the file * @param bool $overwrite * * @return bool @@ -84,14 +89,18 @@ public function rename($path, $newpath, $overwrite = false) return $adapter->rename($path, $newpath, $overwrite); }; - return $this->runChainAndLog($fn); + return $this->logOnFalse( + $this->runChain($fn), + "Impossible rename file from {from} to {to}.", + ['from' => $path, 'to' => $newpath] + ); } /** * Copy a file. * - * @param string $path Path to the existing file - * @param string $newpath The new path of the file + * @param string $path Path to the existing file + * @param string $newpath The new path of the file * * @return bool * @@ -103,7 +112,11 @@ public function copy($path, $newpath) return $adapter->copy($path, $newpath); }; - return $this->runChainAndLog($fn); + return $this->logOnFalse( + $this->runChain($fn), + "Impossible copy file from {from} to {to}.", + ['from' => $path, 'to' => $newpath] + ); } /** @@ -121,7 +134,7 @@ public function delete($path) return $adapter->delete($path); }; - return $this->runChainAndLog($fn); + return $this->logOnFalse($this->runChain($fn), "Impossible delete file {file}.", ['file' => $path]); } /** @@ -140,7 +153,7 @@ public function put($path, $contents) return $adapter->put($path, $contents); }; - return $this->runChainAndLog($fn); + return $this->logOnFalse($this->runChain($fn), "Impossible put file {file}.", ['file' => $path]); } /** @@ -149,7 +162,7 @@ public function put($path, $contents) * @param string $path The path to the file * @param resource $resource The file handle * - * @throws \Cmp\Storage\InvalidArgumentException Thrown if $resource is not a resource + * @throws \InvalidArgumentException Thrown if $resource is not a resource * * @return bool True on success, false on failure */ @@ -159,57 +172,6 @@ public function putStream($path, $resource) return $adapter->putStream($path, $resource); }; - return $this->runChainAndLog($fn); - } - - /** - * Executes the operation in all adapters, returning on the first success or false if at least one executed the - * operation without raising exceptions. - * - * @param callable $fn - * - * @return mixed If all adapters raised exceptions, the first one will be thrown - * - * @throws bool - */ - private function runChainAndLog(callable $fn) - { - $firstException = false; - $call = false; - $result = false; - foreach ($this->getAdapters() as $adapter) { - try { - $result = $fn($adapter); - $call = true; - if ($result !== false) { - return $result; - } - } catch (\Exception $exception) { - if (!$firstException) { - $firstException = $exception; - } - $this->logAdapterException($adapter, $exception); - } - } - - // Result will be set if at least one adapters executed the operation without exceptions - if ($call) { - return $result; - } - - throw $firstException; - } - - /** - * @param \Cmp\Storage\VirtualStorageInterface $adapter - * @param \Exception $e - */ - private function logAdapterException($adapter, $e) - { - $this->log( - LogLevel::ERROR, - 'Adapter "'.$adapter->getName().'" fails.', - ['exception' => $e] - ); + return $this->logOnFalse($this->runChain($fn), "Impossible put file stream {file}.", ['file' => $path]); } } diff --git a/src/Cmp/Storage/Strategy/RunAndLogTrait.php b/src/Cmp/Storage/Strategy/RunAndLogTrait.php new file mode 100644 index 0000000..1b972c8 --- /dev/null +++ b/src/Cmp/Storage/Strategy/RunAndLogTrait.php @@ -0,0 +1,129 @@ +getAdapters() as $adapter) { + try { + $result = $fn($adapter); + $call = true; + if ($result !== false) { + return $result; + } + } catch (\Exception $exception) { + if (!$firstException) { + $firstException = $exception; + } + $this->logAdapterException($adapter, $exception); + } + } + + // Result will be set if at least one adapters executed the operation without exceptions + if ($call) { + return $result; + } + + throw $firstException; + } + + /** + * @param AdapterInterface $adapter + * @param \Exception $exception + */ + private function logAdapterException(AdapterInterface $adapter, \Exception $exception) + { + $this->logger->log( + LogLevel::ERROR, + 'Adapter "'.$adapter->getName().'" fails.', + ['exception' => $exception] + ); + } + + /** + * @param $result + * @param $msg + * @param array $context + * + * @return mixed + */ + private function logOnFalse($result, $msg, array $context = []) + { + if ($result) { + return $result; + } + + $this->logger->log(LogLevel::ERROR, $msg, $context); + + return $result; + } + + /** + * @param callable $fn + * + * @return bool + */ + private function runAll(callable $fn) + { + $result = false; + + foreach ($this->getAdapters() as $adapter) { + try { + $result = $fn($adapter) || $result; + } catch (\Exception $e) { + $this->logAdapterException($adapter, $e); + } + } + + return $result; + } + + /** + * Gets one file from all the adapters. + * + * @param callable $fn + * @param string $path + * + * @return mixed + * + * @throws AdapterException|\Exception + */ + private function runOne(callable $fn, $path) + { + $defaultException = new FileNotFoundException($path); + foreach ($this->getAdapters() as $adapter) { + try { + $file = $fn($adapter); + if ($file !== false) { + return $file; + } + } catch (\Exception $exception) { + $defaultException = new AdapterException($path, $exception); + $this->logAdapterException($adapter, $exception); + } + } + + throw $defaultException; + } +} diff --git a/src/Cmp/Storage/VirtualPath.php b/src/Cmp/Storage/VirtualPath.php index cd6017d..0a1ac74 100644 --- a/src/Cmp/Storage/VirtualPath.php +++ b/src/Cmp/Storage/VirtualPath.php @@ -2,9 +2,6 @@ namespace Cmp\Storage; -use Cmp\Storage\Exception\InvalidPathException; -use Cmp\Storage\Exception\RelativePathNotAllowed; - /** * Class VirtualPath. */ @@ -22,20 +19,10 @@ class VirtualPath */ public function __construct($path) { - $this->path = $this->makePathAbsolute($path); $this->path = $this->canonicalize($this->path); } - /** - * @return string - */ - public function getPath() - { - return $this->path; - } - - /** * @param $path * @@ -48,7 +35,6 @@ public function makePathAbsolute($path) } return $path; - } /** @@ -76,9 +62,9 @@ public function isAbsolutePath($path) */ private function canonicalize($path) { - $path = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $path); - $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); - $absolutes = array(); + $path = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $path); + $parts = array_filter(explode(DIRECTORY_SEPARATOR, $path), 'strlen'); + $absolutes = []; foreach ($parts as $part) { if ('.' == $part) { continue; @@ -106,4 +92,12 @@ public function isChild(VirtualPath $path) return strpos($path->getPath(), $this->getPath()) === 0; } + + /** + * @return string + */ + public function getPath() + { + return $this->path; + } } diff --git a/src/Cmp/Storage/VirtualStorageInterface.php b/src/Cmp/Storage/VirtualStorageInterface.php index 292c5f4..9e4a968 100644 --- a/src/Cmp/Storage/VirtualStorageInterface.php +++ b/src/Cmp/Storage/VirtualStorageInterface.php @@ -2,6 +2,10 @@ namespace Cmp\Storage; +use Cmp\Storage\Exception\FileNotFoundException; +use Cmp\Storage\Exception\InvalidPathException; +use InvalidArgumentException; + /** * Interface VirtualStorageInterface. */ @@ -21,7 +25,7 @@ public function exists($path); * * @param string $path The path to the file * - * @throws \Cmp\Storage\FileNotFoundException + * @throws FileNotFoundException * * @return string The file contents or false on failure */ @@ -32,7 +36,7 @@ public function get($path); * * @param string $path The path to the file * - * @throws \Cmp\Storage\FileNotFoundException + * @throws FileNotFoundException * * @return resource The path resource or false on failure */ @@ -41,20 +45,19 @@ public function getStream($path); /** * Rename a file. * - * @param string $path Path to the existing file - * @param string $newpath The new path of the file + * @param string $path Path to the existing file + * @param string $newpath The new path of the file * @param bool $overwrite * * @return bool */ public function rename($path, $newpath, $overwrite = false); - /** * Rename a file. * - * @param string $path Path to the existing file - * @param string $newpath The destination path of the copy + * @param string $path Path to the existing file + * @param string $newpath The destination path of the copy * * @return bool */ @@ -77,7 +80,7 @@ public function delete($path); * * @return bool True on success, false on failure * - * @throws \Cmp\Storage\InvalidPathException + * @throws InvalidPathException */ public function put($path, $contents); @@ -87,8 +90,8 @@ public function put($path, $contents); * @param string $path The path to the file * @param resource $resource The file handle * - * @throws \Cmp\Storage\InvalidPathException - * @throws \Cmp\Storage\InvalidArgumentException Thrown if $resource is not a resource + * @throws InvalidPathException + * @throws InvalidArgumentException Thrown if $resource is not a resource * * @return bool True on success, false on failure */ diff --git a/test/spec/Cmp/Storage/StorageBuilderSpec.php b/test/spec/Cmp/Storage/StorageBuilderSpec.php index 6208df2..34149d6 100644 --- a/test/spec/Cmp/Storage/StorageBuilderSpec.php +++ b/test/spec/Cmp/Storage/StorageBuilderSpec.php @@ -28,11 +28,6 @@ public function it_allows_setting_a_logger(LoggerInterface $l) $this->getLogger()->shouldBe($l); } - public function it_allows_add_builtin_adapters() - { - $this->addAdapter('FileSystem')->shouldBe($this); - } - public function it_allows_add_already_initialized_adapter(AdapterInterface $vi) { $this->addAdapter($vi)->shouldBe($this); @@ -40,9 +35,8 @@ public function it_allows_add_already_initialized_adapter(AdapterInterface $vi) public function it_throw_and_exception_when_the_adapter_is_not_valid() { - $this->shouldThrow('\Cmp\Storage\Exception\StorageAdapterNotFoundException')->during('addAdapter', ['string']); + $this->shouldThrow('\Cmp\Storage\Exception\InvalidStorageAdapterException')->during('addAdapter', ['string']); $s = new \stdClass(); - $this->shouldThrow('\Cmp\Storage\Exception\InvalidStorageAdapterException')->during('addAdapter', [$s, []]); $this->shouldThrow('\Cmp\Storage\Exception\InvalidStorageAdapterException')->during('addAdapter', [$s]); } @@ -57,7 +51,10 @@ public function it_injects_logger_if_is_possible(LoggerInterface $loggerInterfac $this->setLogger($loggerInterface); $this->addAdapter($adapterWithLogger); + $this->build(); + $adapterWithLogger->setLogger(Argument::any())->shouldHaveBeenCalled(); + } public function it_loads_the_the_default_if_no_other_has_been_been_added(AbstractStorageCallStrategy $callStrategy) @@ -95,12 +92,4 @@ public function it_allows_specify_different_call_strategies( $storage->getStrategyName()->shouldBe($strategyName); } - public function it_builds_a_virtual_storage_with_specific_call_strategy_and_logger_provider( - AdapterInterface $a, - AbstractStorageCallStrategy $callStrategy, - LoggerInterface $loggerInterface - ) { - $this->addAdapter($a); - $this->build($callStrategy, $loggerInterface)->shouldHaveType('\Cmp\Storage\VirtualStorageInterface'); - } } diff --git a/test/spec/Cmp/Storage/Strategy/CallAllStrategySpec.php b/test/spec/Cmp/Storage/Strategy/CallAllStrategySpec.php index 59f9206..f0f93ad 100644 --- a/test/spec/Cmp/Storage/Strategy/CallAllStrategySpec.php +++ b/test/spec/Cmp/Storage/Strategy/CallAllStrategySpec.php @@ -2,6 +2,7 @@ namespace spec\Cmp\Storage\Strategy; +use Cmp\Storage\Adapter\FileSystemAdapter; use Cmp\Storage\AdapterInterface; use Cmp\Storage\Exception\FileNotFoundException; use PhpSpec\ObjectBehavior; @@ -81,7 +82,7 @@ public function it_wraps_the_rename_call( AdapterInterface $adapter2, AdapterInterface $adapter3 ) { - $path = '/b/c'; + $path = '/b/c'; $newpath = '/b/d'; $adapter1->rename($path, $newpath, false)->willReturn(true); $adapter2->rename($path, $newpath, false)->willReturn(true); @@ -94,13 +95,12 @@ public function it_wraps_the_rename_call( $this->rename($path, $newpath, true)->shouldBe(true); } - public function it_wraps_the_copy_call( AdapterInterface $adapter1, AdapterInterface $adapter2, AdapterInterface $adapter3 ) { - $path = '/b/c'; + $path = '/b/c'; $newpath = '/b/d'; $adapter1->copy($path, $newpath)->willReturn(true); $adapter2->copy($path, $newpath)->willReturn(true); @@ -108,7 +108,6 @@ public function it_wraps_the_copy_call( $this->copy($path, $newpath)->shouldBe(true); } - public function it_wraps_the_delete_call( AdapterInterface $adapter1, AdapterInterface $adapter2, @@ -138,7 +137,7 @@ public function it_wraps_the_get_and_getStream_calls( AdapterInterface $adapter2, AdapterInterface $adapter3 ) { - $path = '/b/c'; + $path = '/b/c'; $contents = 'hi!'; $adapter1->get($path)->willReturn($contents); $this->get($path)->shouldBe($contents); @@ -169,9 +168,9 @@ public function it_wraps_the_put_and_putStream_calls( AdapterInterface $adapter2, AdapterInterface $adapter3 ) { - $path = '/b/c'; + $path = '/b/c'; $contents = 'hi!'; - $stream = 'stream'; + $stream = 'stream'; $adapter1->put($path, $contents)->willReturn(true); $adapter2->put($path, $contents)->willReturn(true); $adapter3->put($path, $contents)->willReturn(true); @@ -182,4 +181,21 @@ public function it_wraps_the_put_and_putStream_calls( $adapter3->putStream($path, $stream)->willReturn(true); $this->putStream($path, $stream)->shouldBe(true); } + + public function it_log_on_action_error(FileSystemAdapter $dummyAdapter, LoggerInterface $logger) + { + + $dummyAdapter->getName()->willReturn("Dummy adapter"); + $this->setLogger($logger); + $dummyAdapter->setLogger($logger); + $logger->log(LogLevel::INFO,'Add adapter "{adapter}" to strategy "{strategy}".',["adapter" => "Dummy adapter", "strategy" => "CallAllStrategy"])->shouldBeCalled(); + $this->addAdapter($dummyAdapter); + + $path = '/b/c'; + $contents = 'hi!'; + $dummyAdapter->put($path, $contents)->willReturn(false); + $logger->log(LogLevel::ERROR,"Impossible put file {file}.",["file" => "/b/c"])->shouldBeCalled(); + $this->put($path, $contents)->shouldBe(false); + + } }