diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..40a4326 --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,14 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Exception; + +interface ExceptionInterface +{ +} diff --git a/src/Exception/ExistingNodeException.php b/src/Exception/ExistingNodeException.php new file mode 100644 index 0000000..890e66b --- /dev/null +++ b/src/Exception/ExistingNodeException.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Exception; + +use Exception; +use OutOfBoundsException; +use Vfs\Node\NodeContainerInterface; + +class ExistingNodeException extends OutOfBoundsException implements ExceptionInterface +{ + protected $container; + protected $name; + + /** + * @param string $name + * @param NodeContainerInterface $container + * @param integer $code + * @param Exception $previous + */ + public function __construct($name, NodeContainerInterface $container, $code = 0, Exception $previous = null) + { + $this->container = $container; + $this->name = $name; + + $message = sprintf('Node with name "%s" already exists in container.', $name); + + parent::__construct($message, $code, $previous); + } + + /** + * @return NodeContainerInterface + */ + public function getContainer() + { + return $this->container; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } +} diff --git a/src/Exception/MissingNodeException.php b/src/Exception/MissingNodeException.php new file mode 100644 index 0000000..7d342cd --- /dev/null +++ b/src/Exception/MissingNodeException.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Exception; + +use Exception; +use OutOfRangeException; +use Vfs\Node\NodeContainerInterface; + +class MissingNodeException extends OutOfRangeException implements ExceptionInterface +{ + protected $container; + protected $name; + + /** + * @param string $name + * @param NodeContainerInterface $container + * @param integer $code + * @param Exception $previous + */ + public function __construct($name, NodeContainerInterface $container, $code = 0, Exception $previous = null) + { + $this->container = $container; + $this->name = $name; + + $message = sprintf('Node with name "%s" doesn\'t exist in container.', $name); + + parent::__construct($message, $code, $previous); + } + + /** + * @return NodeContainerInterface + */ + public function getContainer() + { + return $this->container; + } + + /** + * @return string + */ + public function getName() + { + return $this->name; + } +} diff --git a/src/Exception/RegisteredSchemeException.php b/src/Exception/RegisteredSchemeException.php new file mode 100644 index 0000000..2246509 --- /dev/null +++ b/src/Exception/RegisteredSchemeException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Exception; + +use Exception; +use OutOfBoundsException; + +class RegisteredSchemeException extends OutOfBoundsException implements ExceptionInterface +{ + protected $scheme; + + /** + * @param string $scheme + * @param integer $code + * @param Exception $previous + */ + public function __construct($scheme, $code = 0, Exception $previous = null) + { + $this->scheme = $scheme; + + $message = sprintf('File system with scheme "%s" has already been registered.', $scheme); + + parent::__construct($message, $code, $previous); + } + + /** + * @return string + */ + public function getScheme() + { + return $this->scheme; + } +} diff --git a/src/Exception/UnopenedHandleException.php b/src/Exception/UnopenedHandleException.php new file mode 100644 index 0000000..59b5c8a --- /dev/null +++ b/src/Exception/UnopenedHandleException.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Exception; + +use Exception; +use RuntimeException; +use Vfs\Stream\HandleInterface; + +class UnopenedHandleException extends RuntimeException implements ExceptionInterface +{ + protected $handle; + protected $url; + + /** + * @param HandleInterface $handle + * @param string $url + * @param integer $code + * @param Exception $previous + */ + public function __construct(HandleInterface $handle, $url, $code = 0, Exception $previous = null) + { + $this->handle = $handle; + $this->url = $url; + + $message = sprintf('Handle at url "%s" hasn\'t been opened.', $url); + + parent::__construct($message, $code, $previous); + } + + /** + * @return HandleInterface + */ + public function getHandle() + { + return $this->handle; + } + + /** + * @return string + */ + public function getUrl() + { + return $this->url; + } +} diff --git a/src/Exception/UnregisteredSchemeException.php b/src/Exception/UnregisteredSchemeException.php new file mode 100644 index 0000000..7c8d097 --- /dev/null +++ b/src/Exception/UnregisteredSchemeException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Exception; + +use Exception; +use OutOfRangeException; + +class UnregisteredSchemeException extends OutOfRangeException implements ExceptionInterface +{ + protected $scheme; + + /** + * @param string $scheme + * @param integer $code + * @param Exception $previous + */ + public function __construct($scheme, $code = 0, Exception $previous = null) + { + $this->scheme = $scheme; + + $message = sprintf('File system with scheme "%s" has not been registered.', $scheme); + + parent::__construct($message, $code, $previous); + } + + /** + * @return string + */ + public function getScheme() + { + return $this->scheme; + } +} diff --git a/src/FileSystem.php b/src/FileSystem.php new file mode 100644 index 0000000..eb95e09 --- /dev/null +++ b/src/FileSystem.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs; + +use Psr\Log\LoggerInterface; +use Vfs\Exception\RegisteredSchemeException; +use Vfs\Exception\UnregisteredSchemeException; +use Vfs\Node\Factory\NodeFactoryInterface; +use Vfs\Node\Walker\NodeWalkerInterface; + +class FileSystem implements FileSystemInterface +{ + protected $factory; + protected $registry; + protected $logger; + protected $scheme; + protected $walker; + protected $wrapperClass; + + /** + * @param string $scheme + * @param string $wrapperClass + * @param NodeFactoryInterface $factory + * @param NodeWalkerInterface $walker + * @param RegistryInterface $registry + * @param LoggerInterface $logger + */ + public function __construct( + $scheme, + $wrapperClass, + NodeFactoryInterface $factory, + NodeWalkerInterface $walker, + RegistryInterface $registry, + LoggerInterface $logger + ) { + $this->wrapperClass = $wrapperClass; + $this->scheme = $this->formatScheme($scheme); + $this->walker = $walker; + $this->logger = $logger; + $this->factory = $factory; + $this->registry = $registry; + + $this->root = $factory->buildDirectory(); + } + + /** + * @param string $scheme + * @return FileSystem + */ + public static function factory($scheme = self::SCHEME) + { + $builder = new FileSystemBuilder($scheme); + + $fs = $builder->build(); + $fs->mount(); + + return $fs; + } + + /** + * {@inheritdoc} + */ + public function get($path) + { + return $this->walker->findNode($this->root, $path); + } + + /** + * {@inheritdoc} + */ + public function getLogger() + { + return $this->logger; + } + + /** + * {@inheritdoc} + */ + public function getNodeFactory() + { + return $this->factory; + } + + /** + * {@inheritdoc} + */ + public function getNodeWalker() + { + return $this->walker; + } + + /** + * @return string + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * {@inheritdoc} + */ + public function mount() + { + if ($this->registry->has($this->scheme) || in_array($this->scheme, stream_get_wrappers())) { + throw new RegisteredSchemeException($this->scheme); + } + + if (stream_wrapper_register($this->scheme, $this->wrapperClass)) { + $this->registry->add($this->scheme, $this); + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function unmount() + { + if (!$this->registry->has($this->scheme) && !in_array($this->scheme, stream_get_wrappers())) { + throw new UnregisteredSchemeException($this->scheme); + } + + if (stream_wrapper_unregister($this->scheme)) { + $this->registry->remove($this->scheme, $this); + return true; + } + + return false; + } + + /** + * @param string $scheme + * @return string + */ + protected function formatScheme($scheme) + { + return rtrim($scheme, ':/\\'); + } +} diff --git a/src/FileSystemBuilder.php b/src/FileSystemBuilder.php new file mode 100644 index 0000000..8ecea02 --- /dev/null +++ b/src/FileSystemBuilder.php @@ -0,0 +1,232 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs; + +use Psr\Log\LoggerInterface; +use Vfs\Logger\PhpErrorLogger; +use Vfs\Node\Factory\NodeFactory; +use Vfs\Node\Factory\NodeFactoryInterface; +use Vfs\Node\Walker\NodeWalker; +use Vfs\Node\Walker\NodeWalkerInterface; + +class FileSystemBuilder +{ + protected $factory; + protected $registry; + protected $logger; + protected $scheme; + protected $walker; + protected $wrapperClass; + protected $tree = []; + + /** + * @param string $scheme + */ + public function __construct($scheme = FileSystemInterface::SCHEME) + { + $this->setScheme($scheme); + } + + /** + * @return FileSystemInterface + */ + public function build() + { + $fs = new FileSystem( + $this->getScheme(), + $this->getStreamWrapper() ?: $this->buildDefaultStreamWrapper(), + $this->getNodeFactory() ?: $this->buildDefaultNodeFactory(), + $this->getNodeWalker() ?: $this->buildDefaultNodeWalker(), + $this->getRegistry() ?: $this->buildDefaultRegistry(), + $this->getLogger() ?: $this->buildDefaultLogger() + ); + + $root = $fs->get('/'); + $factory = $fs->getNodeFactory(); + foreach ($factory->buildTree($this->getTree()) as $name => $node) { + $root->set($name, $node); + } + + return $fs; + } + + /** + * @return LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } + + /** + * @param LoggerInterface $logger + * @return FileSystemBuilder + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + + return $this; + } + + /** + * @return NodeFactoryInterface + */ + public function getNodeFactory() + { + return $this->factory; + } + + /** + * @param NodeFactoryInterface $factory + * @return FileSystemBuilder + */ + public function setNodeFactory(NodeFactoryInterface $factory) + { + $this->factory = $factory; + + return $this; + } + + /** + * @return NodeWalkerInterface + */ + public function getNodeWalker() + { + return $this->walker; + } + + /** + * @param NodeWalkerInterface $walker + * @return FileSystemBuilder + */ + public function setNodeWalker(NodeWalkerInterface $walker) + { + $this->walker = $walker; + + return $this; + } + + /** + * @return RegistryInterface + */ + public function getRegistry() + { + return $this->registry; + } + + /** + * @param RegistryInterface $registry + * @return FileSystemBuilder + */ + public function setRegistry(RegistryInterface $registry) + { + $this->registry = $registry; + + return $this; + } + + /** + * @return string + */ + public function getScheme() + { + return $this->scheme; + } + + /** + * @param string $scheme + * @return FileSystemBuilder + */ + public function setScheme($scheme) + { + $this->scheme = $scheme; + + return $this; + } + + /** + * @return string + */ + public function getStreamWrapper() + { + return $this->wrapperClass; + } + + /** + * @param string $class + * @return FileSystemBuilder + */ + public function setStreamWrapper($class) + { + $this->wrapperClass = $class; + + return $this; + } + + /** + * @return array + */ + public function getTree() + { + return $this->tree; + } + + /** + * @param array $tree + * @return FileSystemBuilder + */ + public function setTree($tree) + { + $this->tree = $tree; + + return $this; + } + + /** + * @return LoggerInterface + */ + protected function buildDefaultLogger() + { + return new PhpErrorLogger(); + } + + /** + * @return NodeFactoryInterface + */ + protected function buildDefaultNodeFactory() + { + return new NodeFactory(); + } + + /** + * @return NodeWalkerInterface + */ + protected function buildDefaultNodeWalker() + { + return new NodeWalker(); + } + + /** + * @return RegistryInterface + */ + protected function buildDefaultRegistry() + { + return FileSystemRegistry::getInstance(); + } + + /** + * @return string + */ + protected function buildDefaultStreamWrapper() + { + return 'Vfs\\Stream\\StreamWrapper'; + } +} diff --git a/src/FileSystemInterface.php b/src/FileSystemInterface.php new file mode 100644 index 0000000..10729ae --- /dev/null +++ b/src/FileSystemInterface.php @@ -0,0 +1,60 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs; + +use Psr\Log\LoggerInterface; +use Vfs\Exception\RegisteredSchemeException; +use Vfs\Exception\UnregisteredSchemeException; +use Vfs\Node\Factory\NodeFactoryInterface; +use Vfs\Node\Walker\NodeWalkerInterface; +use Vfs\Node\NodeInterface; + +interface FileSystemInterface +{ + const SCHEME = 'vfs'; + + /** + * @param $path + * @return NodeInterface + */ + public function get($path); + + /** + * @return LoggerInterface + */ + public function getLogger(); + + /** + * @return NodeFactoryInterface + */ + public function getNodeFactory(); + + /** + * @return NodeWalkerInterface + */ + public function getNodeWalker(); + + /** + * @return string + */ + public function getScheme(); + + /** + * @return boolean + * @throws RegisteredSchemeException If a mounted file system exists at scheme + */ + public function mount(); + + /** + * @return boolean + * @throws UnregisteredSchemeException If a mounted file system doesn't exist at scheme + */ + public function unmount(); +} diff --git a/src/FileSystemRegistry.php b/src/FileSystemRegistry.php new file mode 100644 index 0000000..7541fb2 --- /dev/null +++ b/src/FileSystemRegistry.php @@ -0,0 +1,85 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs; + +use Vfs\Exception\RegisteredSchemeException; +use Vfs\Exception\UnregisteredSchemeException; + +class FileSystemRegistry implements RegistryInterface +{ + protected static $instance; + protected $registered = []; + + /** + * @param FileSystemRegistry[] $fss + */ + public function __construct(array $fss = []) + { + foreach ($fss as $name => $fs) { + $this->add($name, $fs); + } + } + + /** + * @return FileSystemRegistry + */ + public static function getInstance() + { + if (!self::$instance) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * {@inheritdoc} + */ + public function add($scheme, FileSystemInterface $fs) + { + if ($this->has($scheme)) { + throw new RegisteredSchemeException($scheme); + } + + $this->registered[$scheme] = $fs; + } + + /** + * {@inheritdoc} + */ + public function get($scheme) + { + if (!$this->has($scheme)) { + throw new UnregisteredSchemeException($scheme); + } + + return $this->registered[$scheme]; + } + + /** + * {@inheritdoc} + */ + public function has($scheme) + { + return isset($this->registered[$scheme]); + } + + /** + * {@inheritdoc} + */ + public function remove($scheme) + { + if (!$this->has($scheme)) { + throw new UnregisteredSchemeException($scheme); + } + + unset($this->registered[$scheme]); + } +} diff --git a/src/Logger/PhpErrorLogger.php b/src/Logger/PhpErrorLogger.php new file mode 100644 index 0000000..f63346a --- /dev/null +++ b/src/Logger/PhpErrorLogger.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Logger; + +use Psr\Log\AbstractLogger; +use Psr\Log\LogLevel; + +class PhpErrorLogger extends AbstractLogger +{ + /** + * @param mixed $level + * @param string $message + * @param array $context + */ + public function log($level, $message, array $context = []) + { + switch ($level) { + case LogLevel::EMERGENCY: + case LogLevel::ALERT: + case LogLevel::CRITICAL: + case LogLevel::ERROR: + trigger_error($this->format($message, $context), E_USER_ERROR); + break; + case LogLevel::WARNING: + trigger_error($this->format($message, $context), E_USER_WARNING); + break; + case LogLevel::NOTICE: + case LogLevel::INFO: + case LogLevel::DEBUG: + trigger_error($this->format($message, $context), E_USER_NOTICE); + break; + } + } + + /** + * @param string $message + * @param array $context + * @return string + */ + protected function format($message, array $context) + { + foreach ($context as $key => $value) { + $message = str_replace(sprintf('{%s}', $key), $value, $message); + } + + return $message . $this->formatTrace(debug_backtrace(false)); + } + + /** + * @param array $backtrace + * @return string + */ + protected function formatTrace(array $backtrace) + { + $index = min((count($backtrace) + 1), 6); + $origin = $backtrace[$index]; + + $file = isset($origin['file']) ? $origin['file'] : 'unknown'; + $line = isset($origin['line']) ? $origin['line'] : 0; + + return sprintf(' in %s on line %d; triggered', $file, $line); + } +} diff --git a/src/Node/Directory.php b/src/Node/Directory.php new file mode 100644 index 0000000..9efa9a5 --- /dev/null +++ b/src/Node/Directory.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +use ArrayIterator; +use DateTime; +use Vfs\Exception\ExistingNodeException; +use Vfs\Exception\MissingNodeException; + +class Directory implements NodeContainerInterface +{ + const DOT_SELF = '.'; + const DOT_UP = '..'; + + protected $dateAccessed; + protected $dateCreated; + protected $dateModified; + protected $mode; + protected $nodes = []; + + /** + * @param NodeInterface[] $nodes + */ + public function __construct(array $nodes = []) + { + $this->mode = self::TYPE_BLOCK | self::TYPE_DIR; + + $this->dateAccessed = new DateTime(); + $this->dateCreated = new DateTime(); + $this->dateModified = new DateTime(); + + foreach ($nodes as $name => $node) { + $this->add($name, $node); + } + + $this->set(self::DOT_SELF, $this); + } + + /** + * {@inheritdoc} + */ + public function add($name, NodeInterface $node) + { + if ($this->has($name)) { + throw new ExistingNodeException($name, $this); + } + + $this->set($name, $node); + } + + /** + * {@inheritdoc} + */ + public function get($name) + { + if (!$this->has($name)) { + throw new MissingNodeException($name, $this); + } + + return $this->nodes[$name]; + } + + /** + * {@inheritdoc} + */ + public function has($name) + { + return isset($this->nodes[$name]); + } + + /** + * {@inheritdoc} + */ + public function remove($name) + { + if (!$this->has($name)) { + throw new MissingNodeException($name, $this); + } + + unset($this->nodes[$name]); + } + + /** + * @param string $name + * @param NodeInterface $node + */ + public function set($name, NodeInterface $node) + { + $this->nodes[$name] = $node; + + if (self::DOT_UP !== $name && $node instanceof NodeContainerInterface) { + $node->set(self::DOT_UP, $this); + } + } + + /** + * {@inheritdoc} + */ + public function getDateAccessed() + { + return $this->dateAccessed; + } + + /** + * {@inheritdoc} + */ + public function getDateCreated() + { + return $this->dateCreated; + } + + /** + * {@inheritdoc} + */ + public function getDateModified() + { + return $this->dateModified; + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + return new ArrayIterator($this->nodes); + } + + /** + * {@inheritdoc} + */ + public function getMode() + { + return $this->mode; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + $size = 0; + + foreach ($this->nodes as $name => $node) { + if (!in_array($name, [self::DOT_SELF, self::DOT_UP])) { + $size += $node->getSize(); + } + } + + return $size; + } +} diff --git a/src/Node/Factory/NodeFactory.php b/src/Node/Factory/NodeFactory.php new file mode 100644 index 0000000..26a5a5d --- /dev/null +++ b/src/Node/Factory/NodeFactory.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node\Factory; + +use LogicException; +use Vfs\Node\Directory; +use Vfs\Node\File; +use Vfs\Node\NodeContainerInterface; +use Vfs\Node\NodeInterface; + +class NodeFactory implements NodeFactoryInterface +{ + /** + * @param NodeInterface[] $children + * @return NodeContainerInterface + */ + public function buildDirectory(array $children = []) + { + return new Directory($children); + } + + /** + * @param string $content + * @return NodeInterface + */ + public function buildFile($content = '') + { + return new File($content); + } + + /** + * @param string $content + * @return NodeInterface + */ + public function buildLink($content = '') + { + throw new LogicException('Symlinks aren\'t supported yet.'); + } + + /** + * @param array $tree + * @return NodeContainerInterface + */ + public function buildTree(array $tree) + { + $nodes = []; + + foreach ($tree as $name => $content) { + if (is_array($content)) { + $nodes[$name] = $this->buildTree($content); + } else { + $nodes[$name] = $this->buildFile($content); + } + } + + return $this->buildDirectory($nodes); + } +} diff --git a/src/Node/Factory/NodeFactoryInterface.php b/src/Node/Factory/NodeFactoryInterface.php new file mode 100644 index 0000000..a74ac99 --- /dev/null +++ b/src/Node/Factory/NodeFactoryInterface.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node\Factory; + +use Vfs\Node\NodeContainerInterface; +use Vfs\Node\NodeInterface; + +interface NodeFactoryInterface +{ + /** + * @param NodeInterface[] $children + * @return NodeContainerInterface + */ + public function buildDirectory(array $children = []); + + /** + * @param string $content + * @return NodeInterface + */ + public function buildFile($content = ''); + + /** + * @param string $content + * @return NodeInterface + */ + public function buildLink($content = ''); +} diff --git a/src/Node/File.php b/src/Node/File.php new file mode 100644 index 0000000..d502314 --- /dev/null +++ b/src/Node/File.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +use DateTime; + +class File implements FileInterface +{ + protected $dateAccessed; + protected $dateCreated; + protected $dateModified; + protected $content; + protected $mode; + + /** + * @param string $content + */ + public function __construct($content = '') + { + $this->content = (string) $content; + $this->mode = self::TYPE_BLOCK | self::TYPE_FILE; + + $this->dateAccessed = new DateTime(); + $this->dateCreated = new DateTime(); + $this->dateModified = new DateTime(); + } + + /** + * {@inheritdoc} + */ + public function getContent() + { + return $this->content; + } + + /** + * {@inheritdoc} + */ + public function setContent($content) + { + $this->content = (string) $content; + } + + /** + * {@inheritdoc} + */ + public function getDateAccessed() + { + return $this->dateAccessed; + } + + /** + * {@inheritdoc} + */ + public function getDateCreated() + { + return $this->dateCreated; + } + + /** + * {@inheritdoc} + */ + public function getDateModified() + { + return $this->dateModified; + } + + /** + * {@inheritdoc} + */ + public function getMode() + { + return $this->mode; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return strlen($this->content); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->content; + } +} diff --git a/src/Node/FileInterface.php b/src/Node/FileInterface.php new file mode 100644 index 0000000..6b38868 --- /dev/null +++ b/src/Node/FileInterface.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +interface FileInterface extends NodeInterface +{ + /** + * @return string + */ + public function getContent(); + + /** + * @param string $content + */ + public function setContent($content); + + /** + * @return string The file content + */ + public function __toString(); +} diff --git a/src/Node/LinkInterface.php b/src/Node/LinkInterface.php new file mode 100644 index 0000000..762493e --- /dev/null +++ b/src/Node/LinkInterface.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +interface LinkInterface extends NodeInterface +{ + /** + * @return NodeInterface + */ + public function getTarget(); +} diff --git a/src/Node/NodeContainerInterface.php b/src/Node/NodeContainerInterface.php new file mode 100644 index 0000000..584ea98 --- /dev/null +++ b/src/Node/NodeContainerInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +use DateTime; +use IteratorAggregate; +use Vfs\Exception\ExistingNodeException; +use Vfs\Exception\MissingNodeException; + +interface NodeContainerInterface extends NodeInterface, IteratorAggregate +{ + /** + * @param string $name + * @param NodeInterface $node + * @throws ExistingNodeException If a node exists in container with name + */ + public function add($name, NodeInterface $node); + + /** + * @param string $name + * @throws MissingNodeException If a node doesn't exist in container with name + */ + public function get($name); + + /** + * @param string $name + * @return boolean + */ + public function has($name); + + /** + * @param string $name + * @throws MissingNodeException If a node doesn't exist in container with name + */ + public function remove($name); + + /** + * @param string $name + * @param NodeInterface $node + */ + public function set($name, NodeInterface $node); +} diff --git a/src/Node/NodeInterface.php b/src/Node/NodeInterface.php new file mode 100644 index 0000000..c6be255 --- /dev/null +++ b/src/Node/NodeInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +use DateTime; + +interface NodeInterface extends StatInterface +{ + /** + * @return DateTime + */ + public function getDateAccessed(); + + /** + * @return DateTime + */ + public function getDateCreated(); + + /** + * @return DateTime + */ + public function getDateModified(); + + /** + * @return integer + */ + public function getSize(); +} diff --git a/src/Node/StatInterface.php b/src/Node/StatInterface.php new file mode 100644 index 0000000..84a9429 --- /dev/null +++ b/src/Node/StatInterface.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node; + +/** + * @link http://www.gnu.org/software/libc/manual/html_node/Permission-Bits.html + */ +interface StatInterface +{ + const S_IFMT = 0170000; + const S_IFLNK = 0120000; + const S_IFREG = 0100000; + const S_IFDIR = 0040000; + const S_IRUSR = 0000400; + const S_IWUSR = 0000200; + const S_IXUSR = 0000100; + const S_IRGRP = 0000040; + const S_IWGRP = 0000020; + const S_IXGRP = 0000010; + const S_IROTH = 0000004; + const S_IWOTH = 0000002; + const S_IXOTH = 0000001; + + const GROUP_EXEC = self::S_IXGRP; + const GROUP_WRITE = self::S_IWGRP; + const GROUP_READ = self::S_IRGRP; + const OTHER_EXEC = self::S_IXOTH; + const OTHER_WRITE = self::S_IWOTH; + const OTHER_READ = self::S_IROTH; + const USER_EXEC = self::S_IXUSR; + const USER_READ = self::S_IRUSR; + const USER_WRITE = self::S_IWUSR; + const TYPE_BLOCK = self::S_IFMT; + const TYPE_FILE = self::S_IFREG; + const TYPE_DIR = self::S_IFDIR; + const TYPE_LINK = self::S_IFLNK; + + /** + * @return integer + */ + public function getMode(); +} diff --git a/src/Node/Walker/NodeWalker.php b/src/Node/Walker/NodeWalker.php new file mode 100644 index 0000000..7434613 --- /dev/null +++ b/src/Node/Walker/NodeWalker.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node\Walker; + +use Vfs\Exception\InvalidNodeTypeException; +use Vfs\Node\NodeContainerInterface; +use Vfs\Node\NodeInterface; + +class NodeWalker implements NodeWalkerInterface +{ + protected $separator; + + /** + * @param string $separator + */ + public function __construct($separator = DIRECTORY_SEPARATOR) + { + $this->separator = $separator; + } + + /** + * @param NodeInterface $root + * @param string $path + * @return NodeInterface + */ + public function findNode(NodeInterface $root, $path) + { + $parts = $this->splitPath($path); + $node = $root; + + return $this->walkPath($root, $path, function (NodeInterface $node, $name) { + if (!$node instanceof NodeContainerInterface || !$node->has($name)) { + return null; + } + + return $node->get($name); + }); + } + + /** + * @param NodeInterface $root + * @param string $path + * @param callable $fn + * @return NodeInterface + */ + public function walkPath(NodeInterface $root, $path, callable $fn) + { + $parts = $this->splitPath($path); + $name = current($parts); + $node = $root; + + while ($node && $name) { + $node = $fn($node, $name); + $name = next($parts); + } + + return $node; + } + + /** + * @param string $path + * @return string[] + */ + protected function splitPath($path) + { + return array_filter(explode($this->separator, $path)); + } +} diff --git a/src/Node/Walker/NodeWalkerInterface.php b/src/Node/Walker/NodeWalkerInterface.php new file mode 100644 index 0000000..a67cfce --- /dev/null +++ b/src/Node/Walker/NodeWalkerInterface.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Node\Walker; + +use Vfs\Node\NodeInterface; + +interface NodeWalkerInterface +{ + /** + * @param NodeInterface $root + * @param string $path + * @return NodeInterface + */ + public function findNode(NodeInterface $root, $path); +} diff --git a/src/RegistryInterface.php b/src/RegistryInterface.php new file mode 100644 index 0000000..8ca4a35 --- /dev/null +++ b/src/RegistryInterface.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs; + +use Vfs\Exception\RegisteredSchemeException; +use Vfs\Exception\UnregisteredSchemeException; + +interface RegistryInterface +{ + /** + * @param string $scheme + * @param FileSystemInterface $fs + * @throws RegisteredSchemeException If a mounted file system exists at scheme + */ + public function add($scheme, FileSystemInterface $fs); + + /** + * @param string $scheme + * @return FileSystemInterface + * @throws UnregisteredSchemeException If a mounted file system doesn't exist at scheme + */ + public function get($scheme); + + /** + * @param string $scheme + * @return boolean + */ + public function has($scheme); + + /** + * @param string $scheme + * @return FileSystemInterface + * @throws UnregisteredSchemeException If a mounted file system doesn't exist at scheme + */ + public function remove($scheme); +} diff --git a/src/Stream/AbstractHandle.php b/src/Stream/AbstractHandle.php new file mode 100644 index 0000000..129bd62 --- /dev/null +++ b/src/Stream/AbstractHandle.php @@ -0,0 +1,117 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Stream; + +use Vfs\FileSystemInterface; +use Vfs\Node\NodeInterface; + +abstract class AbstractHandle implements HandleInterface +{ + protected $fs; + protected $node; + protected $mode; + protected $modifier; + protected $path; + protected $scheme; + protected $url; + + /** + * @param FileSystemInterface $fs + * @param string $url + * @param string $mode + */ + public function __construct(FileSystemInterface $fs, $url, $mode = null) + { + $this->fs = $fs; + $this->url = $url; + + list($this->mode, $this->modifier) = $this->parseMode($mode); + list($this->scheme, $this->path) = $this->parseUrl($url); + } + + /** + * @return NodeInterface + */ + public function getNode() + { + return $this->node; + } + + /** + * @param string $origin + * @param string $target + * @return NodeInterface + */ + public function rename($target) + { + $this->node = $this->findNode($this->path); + $parent = $this->fs->get(dirname($this->path)); + + list($_, $targetPath) = $this->parseUrl($target); + $targetParent = $this->fs->get(dirname($targetPath)); + + if (!$this->node || !$targetPath) { + $this->node = null; + $this->warn('rename({origin},{target}): No such file or directory', [ + 'origin' => $this->url, + 'target' => $target + ]); + } else { + $parent->remove(basename($this->path)); + $targetParent->add(basename($targetPath), $this->node); + } + + return $this->node; + } + + /** + * @return NodeInterface + */ + protected function findNode() + { + return $this->fs->get($this->path); + } + + /** + * @param string $mode + * @return string[] + */ + protected function parseMode($mode) + { + return [substr($mode, 0, 1), substr($mode, 1, 2)]; + } + + /** + * @param string $url + * @return string[] + */ + protected function parseUrl($url) + { + $parts = parse_url($url); + $path = null; + $scheme = null; + + if (isset($parts['scheme'])) { + $path = substr($url, strlen($parts['scheme'] . ':/')); + $scheme = $parts['scheme']; + } + + return [$scheme, $path]; + } + + /** + * @param string $message + * @param array $context + */ + protected function warn($message, array $context = []) + { + $this->fs->getLogger()->warning($message, $context); + } +} diff --git a/src/Stream/DirectoryHandle.php b/src/Stream/DirectoryHandle.php new file mode 100644 index 0000000..79664de --- /dev/null +++ b/src/Stream/DirectoryHandle.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Stream; + +use Vfs\Exception\UnopenedHandleException; + +class DirectoryHandle extends AbstractHandle +{ + /** + * @return boolean + */ + public function canRead() + { + return true; + } + + /** + * @param integer $perms + * @param boolean $recursive + * @return NodeInterface + */ + public function create($perms, $recursive = false) + { + $this->node = $this->findNode(); + + if (!$this->node) { + $parentPath = dirname($this->path); + $parent = $this->fs->get($parentPath); + + if (!$parent && $this->checkBit($options, STREAM_MKDIR_RECURSIVE)) { + $parent = $this->buildNodesRecursive($this->fs->get('/'), $this->path); + } + + if ($parent) { + $this->node = $this->fs->getNodeFactory()->buildDirectory(); + $parent->add(basename($this->path), $this->node); + } else { + $this->warn('mkdir({url}): No such file or directory', [ + 'url' => $this->url + ]); + } + } else { + $this->warn('mkdir({url}): File exists', ['url' => $this->url]); + $this->node = null; + } + + return $this->node; + } + + /** + * @return boolean + */ + public function destroy() + { + $this->node = $this->findNode(); + + if (!$this->node) { + return (boolean) $this->warn('rmdir({url}): No such file or directory', [ + 'url' => $this->url + ]); + } elseif (!$this->node instanceof NodeContainerInterface) { + return (boolean) $this->warn('rmdir({url}): Not a directory', [ + 'url' => $this->url + ]); + } + + $parent = $fs->get(dirname($this->path)); + $parent->remove(basename($this->path)); + + return true; + } + + /** + * @return NodeInterface + */ + public function open() + { + return $this->node = $this->findNode(); + } + + /** + * @param integer $offset + * @return string + */ + public function read($offset = 0) + { + if (!$this->node) { + throw new UnopenedHandleException($this, $this->url); + } + + $i = 0; + foreach ($this->node as $name => $node) { + if ($i++ === $offset) { + return $name; + } + } + } + + /** + * @param string $content + * @return boolean + */ + public function write($content) + { + return false; + } + + /** + * @param NodeContainerInterface $root + * @param string $path + * @return NodeContainerInterface + */ + protected function buildNodesRecursive(NodeContainerInterface $root, $path) + { + $factory = $this->fs->getNodeFactory(); + $walker = $this->fs->getNodeWalker(); + + return $walker->walkPath($root, $this->path, function ($node, $name) use ($factory) { + if (!$node->has($name)) { + $node->add($name, $factory->buildDirectory()); + } + + return $node->get($name); + }); + } +} diff --git a/src/Stream/FileHandle.php b/src/Stream/FileHandle.php new file mode 100644 index 0000000..a74dc2d --- /dev/null +++ b/src/Stream/FileHandle.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Stream; + +use Vfs\Exception\UnopenedHandleException; +use Vfs\Node\FileInterface; +use Vfs\Node\NodeContainerInterface; + +class FileHandle extends AbstractHandle +{ + /** + * @return boolean + */ + public function canRead() + { + return self::MODE_READ === $this->mode || self::MOD_EXTENDED === $this->modifier; + } + + /** + * @param integer $perms + * @return NodeInterface + */ + public function create($perms) + { + return $this->node = $this->findOrBuildNode(); + } + + /** + * @return boolean + */ + public function destroy() + { + $this->node = $this->findNode(); + + if (!$this->node) { + return (boolean) $this->warn('unlink({url}): No such file or directory', [ + 'url' => $this->url + ]); + } elseif ($this->node instanceof NodeContainerInterface) { + return (boolean) $this->warn('unlink({url}): Is a directory', [ + 'url' => $this->url + ]); + } + + $parent = $this->fs->get(dirname($this->path)); + $parent->remove(basename($this->path)); + + return true; + } + + /** + * @return NodeInterface + */ + public function open() + { + $this->node = $this->findOrBuildNode(); + + if ($this->node instanceof FileInterface && self::MODE_TRUNCATE === $this->mode) { + $this->node->setContent(''); + } + + return $this->node; + } + + /** + * @param integer $offset + * @param integer $length + * @return string + */ + public function read($offset = 0, $length = null) + { + if (!$this->node) { + throw new UnopenedHandleException($this, $this->url); + } + + if (null !== $length) { + return substr($this->node->getContent(), $offset, $length); + } + + return substr($this->node->getContent(), $offset); + } + + /** + * @param string $content + * @return boolean + */ + public function write($content) + { + if (!$this->node) { + throw new UnopenedHandleException($this, $this->url); + } + + $this->node->setContent($content); + + return true; + } + + /** + * @return NodeInterface + */ + protected function findOrBuildNode() + { + $this->node = $this->fs->get($this->path); + + if ($this->node && self::MODE_WRITE === $this->mode) { + $this->node = null; + } elseif (!$this->node && in_array($this->mode, [ + self::MODE_APPEND, + self::MODE_TRUNCATE, + self::MODE_WRITE, + self::MODE_WRITE_NEW + ])) { + $dir = $this->fs->get(dirname($this->path)); + + if ($dir && $dir instanceof NodeContainerInterface) { + $this->node = $this->fs->getNodeFactory()->buildFile(); + $dir->set(basename($this->path), $this->node); + } + } + + return $this->node; + } +} diff --git a/src/Stream/HandleInterface.php b/src/Stream/HandleInterface.php new file mode 100644 index 0000000..36d2328 --- /dev/null +++ b/src/Stream/HandleInterface.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Stream; + +use Vfs\Node\NodeInterface; + +interface HandleInterface +{ + const MODE_APPEND = 'a'; + const MODE_READ = 'r'; + const MODE_TRUNCATE = 'w'; + const MODE_WRITE = 'x'; + const MODE_WRITE_NEW = 'c'; + const MOD_BINARY = 'b'; + const MOD_EXTENDED = '+'; + const MOD_TEXT = 't'; + + /** + * @return boolean + */ + public function canRead(); + + /** + * @param integer $perms + * @return NodeInterface + */ + public function create($perms); + + /** + * @return boolean + */ + public function destroy(); + + /** + * @return NodeInterface + */ + public function getNode(); + + /** + * @return NodeInterface + */ + public function open(); + + /** + * @param string $origin + * @param string $target + * @return NodeInterface + */ + public function rename($target); + + /** + * @param integer $offset + * @return string + */ + public function read($offset = 0); + + /** + * @param string $content + * @return boolean + */ + public function write($content); +} diff --git a/src/Stream/StreamWrapper.php b/src/Stream/StreamWrapper.php new file mode 100644 index 0000000..e40697b --- /dev/null +++ b/src/Stream/StreamWrapper.php @@ -0,0 +1,345 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Vfs\Stream; + +use Vfs\Node\LinkInterface; +use Vfs\FileSystemRegistry; + +class StreamWrapper +{ + protected $cursor = 0; + protected $handle; + protected $mode; + protected $url; + + /** + * @return boolean + */ + public function dir_closedir() + { + $this->stream_close(); + + return true; + } + + /** + * @param string $url + * @param integer $options + * @return boolean + */ + public function dir_opendir($url, $options) + { + $this->handle = $this->buildDirectoryHandle($url); + + return (boolean) $this->handle->open(); + } + + /** + * @return string + */ + public function dir_readdir() + { + return $this->handle->read($this->cursor++); + } + + /** + * @return boolean + */ + public function dir_rewinddir() + { + $this->cursor = 0; + + return true; + } + + /** + * @param string $url + * @param integer $perms + * @param integer $flags + * @return boolean + */ + public function mkdir($url, $perms, $flags) + { + $this->handle = $this->buildDirectoryHandle($url); + $recursive = $this->checkBit($flags, STREAM_MKDIR_RECURSIVE); + + return (boolean) $this->handle->create($perms, $recursive); + } + + /** + * @param string $origin + * @param string $target + * @return boolean + */ + public function rename($origin, $target) + { + $this->handle = $this->buildFileHandle($origin); + + return (boolean) $this->handle->rename($target); + } + + /** + * @param string $url + * @param integer $options + * @return boolean + */ + public function rmdir($url, $options) + { + $this->handle = $this->buildDirectoryHandle($url); + + return (boolean) $this->handle->destroy(); + } + + /** + * @param integer $cast + * @return resource|boolean + */ + public function stream_cast($cast) + { + return false; // No underlying resource + } + + /** + */ + public function stream_close() + { + $this->cursor = 0; + $this->handle = null; + } + + /** + * @return boolean + */ + public function stream_eof() + { + $node = $this->handle->getNode(); + + return !$node || $node->getSize() <= $this->cursor; + } + + /** + * @return boolean + */ + public function stream_flush() + { + return true; // Non-buffered writing + } + + /** + * @param string $url + * @param string $mode + * @param integer $options + * @param string $openedPath + * @return boolean + */ + public function stream_open($url, $mode, $options, &$openedPath) + { + $this->cursor = 0; + $this->handle = $this->buildFileHandle($url, $mode); + $node = $this->handle->open(); + + if ($node && $this->checkBit($options, STREAM_USE_PATH)) { + $openedPath = $url; + } + + if (isset($mode[0]) && $node && HandleInterface::MODE_APPEND === $mode[0]) { + $this->cursor = $node->getSize(); + } + + return (boolean) $node; + } + + /** + * @param integer $length + * @return string|boolean + */ + public function stream_read($length) + { + if ($this->handle->canRead()) { + $out = $this->handle->read($this->cursor, $length); + $this->cursor += strlen($out); + + return $out; + } + + return false; + } + + /** + * @param integer $offset + * @param integer $whence + * @return boolean + */ + public function stream_seek($offset, $whence = SEEK_SET) + { + switch ($whence) { + case SEEK_SET: + $this->cursor = (integer) $offset; + break; + case SEEK_CUR: + $this->cursor += (integer) $offset; + break; + case SEEK_END: + $length = strlen($this->wrapper->read()); + $this->cursor = $length + (integer) $offset; + break; + default: + return false; + } + + return true; + } + + /** + * @param integer $option + * @param integer $arg1 + * @param integer $arg2 + * @return boolean + */ + public function stream_set_option($option, $arg1, $arg2) + { + return true; + } + + /** + * @param boolean $followLink + * @return array|boolean + */ + public function stream_stat($followLink = false) + { + $node = $this->handle->getNode(); + + if (!$node) { + return false; + } elseif ($followLink && $node instanceof LinkInterface) { + $node = $node->getTarget(); + } + + $stat = [ + 'dev' => 0, + 'ino' => 0, + 'mode' => $node->getMode(), + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $node->getSize(), + 'atime' => $node->getDateAccessed()->getTimestamp(), + 'mtime' => $node->getDateModified()->getTimestamp(), + 'ctime' => $node->getDateCreated()->getTimestamp(), + 'blksize' => -1, + 'blocks' => -1 + ]; + + return array_values($stat) + $stat; + } + + /** + * @return integer + */ + public function stream_tell() + { + return $this->cursor; + } + + /** + * @param integer $size + * @return boolean + */ + public function stream_truncate($size) + { + if ($size > $current) { + $this->handle->write($this->handle->read() . str_repeat("\0", $size - $current)); + } else { + $this->handle->write($this->handle->read(0, $size)); + } + + return true; + } + + /** + * @param string $data + * @return integer + */ + public function stream_write($data) + { + $content = substr($this->handle->read(0, $this->cursor), 0, $this->cursor) . $data; + $written = $this->handle->write($content); + + $this->cursor = strlen($content); + + return $written ? strlen($data) : 0; + } + + /** + * @param string $url + * @return boolean + */ + public function unlink($url) + { + $this->handle = $this->buildFileHandle($url); + + return (boolean) $this->handle->destroy(); + } + + /** + * @param string $url + * @param integer $flags + * @return array + */ + public function url_stat($url, $flags) + { + $this->handle = $this->buildFileHandle($url); + $this->handle->open(); + + return $this->stream_stat(!$this->checkBit($flags, STREAM_URL_STAT_LINK)); + } + + /** + * @param string $url + * @return DirectoryHandle + */ + protected function buildDirectoryHandle($url) + { + return new DirectoryHandle($this->getFileSystemForUrl($url), $url); + } + + /** + * @param string $url + * @param string $mode + * @return FileHandle + */ + protected function buildFileHandle($url, $mode = null) + { + return new FileHandle($this->getFileSystemForUrl($url), $url, $mode); + } + + /** + * @param integer $mask + * @param integer $bit + * @return boolean + */ + protected function checkBit($mask, $bit) + { + return ($mask & $bit) === $bit; + } + + /** + * @param string $url + * @return FileSystemInterface + */ + protected function getFileSystemForUrl($url) + { + $parts = parse_url($url); + $scheme = isset($parts['scheme']) ? $parts['scheme'] : null; + + return FileSystemRegistry::getInstance()->get($scheme); + } +}