3 * This file is part of vfsStream.
5 * For the full copyright and license information, please view the LICENSE
6 * file that was distributed with this source code.
8 * @package org\bovigo\vfs
10 namespace org\bovigo\vfs;
12 * Stream wrapper to mock file system requests.
14 class vfsStreamWrapper
17 * open file for reading
25 * set file pointer to end, append new data
29 * set file pointer to start, overwrite existing data
33 * set file pointer to start, overwrite existing data; or create file if
36 const WRITE_NEW = 'c';
38 * file mode: read only
42 * file mode: write only
46 * file mode: read and write
50 * switch whether class has already been registered as stream wrapper or not
54 protected static $registered = false;
58 * @type vfsStreamContent
60 protected static $root;
66 private static $quota;
68 * file mode: read only, write only, all
74 * shortcut to file container
80 * shortcut to directory container
82 * @type vfsStreamDirectory
86 * shortcut to directory container iterator
88 * @type vfsStreamDirectory
90 protected $dirIterator;
93 * method to register the stream wrapper
95 * Please be aware that a call to this method will reset the root element
97 * If the stream is already registered the method returns silently. If there
98 * is already another stream wrapper registered for the scheme used by
99 * vfsStream a vfsStreamException will be thrown.
101 * @throws vfsStreamException
103 public static function register()
106 self::$quota = Quota::unlimited();
107 if (true === self::$registered) {
111 if (@stream_wrapper_register(vfsStream::SCHEME, __CLASS__) === false) {
112 throw new vfsStreamException('A handler has already been registered for the ' . vfsStream::SCHEME . ' protocol.');
115 self::$registered = true;
119 * Unregisters a previously registered URL wrapper for the vfs scheme.
121 * If this stream wrapper wasn't registered, the method returns silently.
123 * If unregistering fails, or if the URL wrapper for vfs:// was not
124 * registered with this class, a vfsStreamException will be thrown.
126 * @throws vfsStreamException
129 public static function unregister()
131 if (!self::$registered) {
132 if (in_array(vfsStream::SCHEME, stream_get_wrappers())) {
133 throw new vfsStreamException('The URL wrapper for the protocol ' . vfsStream::SCHEME . ' was not registered with this version of vfsStream.');
138 if (!@stream_wrapper_unregister(vfsStream::SCHEME)) {
139 throw new vfsStreamException('Failed to unregister the URL wrapper for the ' . vfsStream::SCHEME . ' protocol.');
142 self::$registered = false;
146 * sets the root content
148 * @param vfsStreamContainer $root
149 * @return vfsStreamContainer
151 public static function setRoot(vfsStreamContainer $root)
159 * returns the root content
161 * @return vfsStreamContainer
163 public static function getRoot()
169 * sets quota for disk space
171 * @param Quota $quota
174 public static function setQuota(Quota $quota)
176 self::$quota = $quota;
180 * returns content for given path
182 * @param string $path
183 * @return vfsStreamContent
185 protected function getContent($path)
187 if (null === self::$root) {
191 if (self::$root->getName() === $path) {
195 if ($this->isInRoot($path) && self::$root->hasChild($path) === true) {
196 return self::$root->getChild($path);
203 * helper method to detect whether given path is in root path
205 * @param string $path
208 private function isInRoot($path)
210 return substr($path, 0, strlen(self::$root->getName())) === self::$root->getName();
214 * returns content for given path but only when it is of given type
216 * @param string $path
218 * @return vfsStreamContent
220 protected function getContentOfType($path, $type)
222 $content = $this->getContent($path);
223 if (null !== $content && $content->getType() === $type) {
231 * splits path into its dirname and the basename
233 * @param string $path
236 protected function splitPath($path)
238 $lastSlashPos = strrpos($path, '/');
239 if (false === $lastSlashPos) {
240 return array('dirname' => '', 'basename' => $path);
243 return array('dirname' => substr($path, 0, $lastSlashPos),
244 'basename' => substr($path, $lastSlashPos + 1)
249 * helper method to resolve a path from /foo/bar/. to /foo/bar
251 * @param string $path
254 protected function resolvePath($path)
257 foreach (explode('/', $path) as $pathPart) {
258 if ('.' !== $pathPart) {
259 if ('..' !== $pathPart) {
260 $newPath[] = $pathPart;
261 } elseif (count($newPath) > 1) {
267 return implode('/', $newPath);
273 * @param string $path the path to open
274 * @param string $mode mode for opening
275 * @param string $options options for opening
276 * @param string $opened_path full path that was actually opened
279 public function stream_open($path, $mode, $options, $opened_path)
281 $extended = ((strstr($mode, '+') !== false) ? (true) : (false));
282 $mode = str_replace(array('t', 'b', '+'), '', $mode);
283 if (in_array($mode, array('r', 'w', 'a', 'x', 'c')) === false) {
284 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
285 trigger_error('Illegal mode ' . $mode . ', use r, w, a, x or c, flavoured with t, b and/or +', E_USER_WARNING);
291 $this->mode = $this->calculateMode($mode, $extended);
292 $path = $this->resolvePath(vfsStream::path($path));
293 $this->content = $this->getContentOfType($path, vfsStreamContent::TYPE_FILE);
294 if (null !== $this->content) {
295 if (self::WRITE === $mode) {
296 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
297 trigger_error('File ' . $path . ' already exists, can not open with mode x', E_USER_WARNING);
304 (self::TRUNCATE === $mode || self::APPEND === $mode) &&
305 $this->content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false
310 if (self::TRUNCATE === $mode) {
311 $this->content->openWithTruncate();
312 } elseif (self::APPEND === $mode) {
313 $this->content->openForAppend();
315 if (!$this->content->isReadable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
316 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
317 trigger_error('Permission denied', E_USER_WARNING);
321 $this->content->open();
327 $content = $this->createFile($path, $mode, $options);
328 if (false === $content) {
332 $this->content = $content;
337 * creates a file at given path
339 * @param string $path the path to open
340 * @param string $mode mode for opening
341 * @param string $options options for opening
344 private function createFile($path, $mode = null, $options = null)
346 $names = $this->splitPath($path);
347 if (empty($names['dirname']) === true) {
348 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
349 trigger_error('File ' . $names['basename'] . ' does not exist', E_USER_WARNING);
355 $dir = $this->getContentOfType($names['dirname'], vfsStreamContent::TYPE_DIR);
357 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
358 trigger_error('Directory ' . $names['dirname'] . ' does not exist', E_USER_WARNING);
362 } elseif ($dir->hasChild($names['basename']) === true) {
363 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
364 trigger_error('Directory ' . $names['dirname'] . ' already contains a director named ' . $names['basename'], E_USER_WARNING);
370 if (self::READ === $mode) {
371 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
372 trigger_error('Can not open non-existing file ' . $path . ' for reading', E_USER_WARNING);
378 if ($dir->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
379 if (($options & STREAM_REPORT_ERRORS) === STREAM_REPORT_ERRORS) {
380 trigger_error('Can not create new file in non-writable path ' . $names['dirname'], E_USER_WARNING);
386 return vfsStream::newFile($names['basename'])->at($dir);
390 * calculates the file mode
392 * @param string $mode opening mode: r, w, a or x
393 * @param bool $extended true if + was set with opening mode
396 protected function calculateMode($mode, $extended)
398 if (true === $extended) {
402 if (self::READ === $mode) {
403 return self::READONLY;
406 return self::WRITEONLY;
412 * @see https://github.com/mikey179/vfsStream/issues/40
414 public function stream_close()
416 $this->content->lock($this, LOCK_UN);
420 * read the stream up to $count bytes
422 * @param int $count amount of bytes to read
425 public function stream_read($count)
427 if (self::WRITEONLY === $this->mode) {
431 if ($this->content->isReadable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
435 return $this->content->read($count);
439 * writes data into the stream
441 * @param string $data
442 * @return int amount of bytes written
444 public function stream_write($data)
446 if (self::READONLY === $this->mode) {
450 if ($this->content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
454 if (self::$quota->isLimited()) {
455 $data = substr($data, 0, self::$quota->spaceLeft(self::$root->sizeSummarized()));
458 return $this->content->write($data);
462 * truncates a file to a given length
464 * @param int $size length to truncate file to
468 public function stream_truncate($size)
470 if (self::READONLY === $this->mode) {
474 if ($this->content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
478 if ($this->content->getType() !== vfsStreamContent::TYPE_FILE) {
482 if (self::$quota->isLimited() && $this->content->size() < $size) {
483 $maxSize = self::$quota->spaceLeft(self::$root->sizeSummarized());
484 if (0 === $maxSize) {
488 if ($size > $maxSize) {
493 return $this->content->truncate($size);
497 * sets metadata like owner, user or permissions
499 * @param string $path
505 public function stream_metadata($path, $option, $var)
507 $path = $this->resolvePath(vfsStream::path($path));
508 $content = $this->getContent($path);
510 case STREAM_META_TOUCH:
511 if (null === $content) {
512 $content = $this->createFile($path, null, STREAM_REPORT_ERRORS);
513 // file creation may not be allowed at provided path
514 if (false === $content) {
519 $currentTime = time();
520 $content->lastModified(((isset($var[0])) ? ($var[0]) : ($currentTime)))
521 ->lastAccessed(((isset($var[1])) ? ($var[1]) : ($currentTime)));
524 case STREAM_META_OWNER_NAME:
527 case STREAM_META_OWNER:
528 if (null === $content) {
532 return $this->doPermChange($path,
534 function() use ($content, $var)
536 $content->chown($var);
540 case STREAM_META_GROUP_NAME:
543 case STREAM_META_GROUP:
544 if (null === $content) {
548 return $this->doPermChange($path,
550 function() use ($content, $var)
552 $content->chgrp($var);
556 case STREAM_META_ACCESS:
557 if (null === $content) {
561 return $this->doPermChange($path,
563 function() use ($content, $var)
565 $content->chmod($var);
575 * executes given permission change when necessary rights allow such a change
577 * @param string $path
578 * @param vfsStreamAbstractContent $content
579 * @param \Closure $change
582 private function doPermChange($path, vfsStreamAbstractContent $content, \Closure $change)
584 if (!$content->isOwnedByUser(vfsStream::getCurrentUser())) {
588 if (self::$root->getName() !== $path) {
589 $names = $this->splitPath($path);
590 $parent = $this->getContent($names['dirname']);
591 if (!$parent->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
601 * checks whether stream is at end of file
605 public function stream_eof()
607 return $this->content->eof();
611 * returns the current position of the stream
615 public function stream_tell()
617 return $this->content->getBytesRead();
621 * seeks to the given offset
627 public function stream_seek($offset, $whence)
629 return $this->content->seek($offset, $whence);
633 * flushes unstored data into storage
637 public function stream_flush()
643 * returns status of stream
647 public function stream_stat()
649 $fileStat = array('dev' => 0,
651 'mode' => $this->content->getType() | $this->content->getPermissions(),
653 'uid' => $this->content->getUser(),
654 'gid' => $this->content->getGroup(),
656 'size' => $this->content->size(),
657 'atime' => $this->content->fileatime(),
658 'mtime' => $this->content->filemtime(),
659 'ctime' => $this->content->filectime(),
663 return array_merge(array_values($fileStat), $fileStat);
667 * retrieve the underlaying resource
669 * Please note that this method always returns false as there is no
670 * underlaying resource to return.
672 * @param int $cast_as
674 * @see https://github.com/mikey179/vfsStream/issues/3
677 public function stream_cast($cast_as)
683 * set lock status for stream
685 * @param int $operation
688 * @see https://github.com/mikey179/vfsStream/issues/6
689 * @see https://github.com/mikey179/vfsStream/issues/31
690 * @see https://github.com/mikey179/vfsStream/issues/40
692 public function stream_lock($operation)
694 if ((LOCK_NB & $operation) == LOCK_NB) {
695 $operation = $operation - LOCK_NB;
698 return $this->content->lock($this, $operation);
702 * sets options on the stream
704 * @param int $option key of option to set
709 * @see https://github.com/mikey179/vfsStream/issues/15
710 * @see http://www.php.net/manual/streamwrapper.stream-set-option.php
712 public function stream_set_option($option, $arg1, $arg2)
715 case STREAM_OPTION_BLOCKING:
718 case STREAM_OPTION_READ_TIMEOUT:
721 case STREAM_OPTION_WRITE_BUFFER:
725 // nothing to do here
732 * remove the data under the given path
734 * @param string $path
737 public function unlink($path)
739 $realPath = $this->resolvePath(vfsStream::path($path));
740 $content = $this->getContent($realPath);
741 if (null === $content) {
742 trigger_error('unlink(' . $path . '): No such file or directory', E_USER_WARNING);
746 if ($content->getType() !== vfsStreamContent::TYPE_FILE) {
747 trigger_error('unlink(' . $path . '): Operation not permitted', E_USER_WARNING);
751 return $this->doUnlink($realPath);
757 * @param string $path
760 protected function doUnlink($path)
762 if (self::$root->getName() === $path) {
763 // delete root? very brave. :)
769 $names = $this->splitPath($path);
770 $content = $this->getContent($names['dirname']);
771 if (!$content->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
776 return $content->removeChild($names['basename']);
780 * rename from one path to another
782 * @param string $path_from
783 * @param string $path_to
785 * @author Benoit Aubuchon
787 public function rename($path_from, $path_to)
789 $srcRealPath = $this->resolvePath(vfsStream::path($path_from));
790 $dstRealPath = $this->resolvePath(vfsStream::path($path_to));
791 $srcContent = $this->getContent($srcRealPath);
792 if (null == $srcContent) {
793 trigger_error(' No such file or directory', E_USER_WARNING);
796 $dstNames = $this->splitPath($dstRealPath);
797 $dstParentContent = $this->getContent($dstNames['dirname']);
798 if (null == $dstParentContent) {
799 trigger_error('No such file or directory', E_USER_WARNING);
802 if (!$dstParentContent->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup())) {
803 trigger_error('Permission denied', E_USER_WARNING);
806 if ($dstParentContent->getType() !== vfsStreamContent::TYPE_DIR) {
807 trigger_error('Target is not a directory', E_USER_WARNING);
811 // remove old source first, so we can rename later
812 // (renaming first would lead to not being able to remove the old path)
813 if (!$this->doUnlink($srcRealPath)) {
817 $dstContent = $srcContent;
818 // Renaming the filename
819 $dstContent->rename($dstNames['basename']);
820 // Copying to the destination
821 $dstParentContent->addChild($dstContent);
826 * creates a new directory
828 * @param string $path
830 * @param int $options
833 public function mkdir($path, $mode, $options)
835 $umask = vfsStream::umask();
837 $permissions = $mode & ~$umask;
839 $permissions = $mode;
842 $path = $this->resolvePath(vfsStream::path($path));
843 if (null !== $this->getContent($path)) {
844 trigger_error('mkdir(): Path vfs://' . $path . ' exists', E_USER_WARNING);
848 if (null === self::$root) {
849 self::$root = vfsStream::newDirectory($path, $permissions);
853 $maxDepth = count(explode('/', $path));
854 $names = $this->splitPath($path);
855 $newDirs = $names['basename'];
858 while ($dir === null && $i < $maxDepth) {
859 $dir = $this->getContent($names['dirname']);
860 $names = $this->splitPath($names['dirname']);
862 $newDirs = $names['basename'] . '/' . $newDirs;
869 || $dir->getType() !== vfsStreamContent::TYPE_DIR
870 || $dir->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
874 $recursive = ((STREAM_MKDIR_RECURSIVE & $options) !== 0) ? (true) : (false);
875 if (strpos($newDirs, '/') !== false && false === $recursive) {
879 vfsStream::newDirectory($newDirs, $permissions)->at($dir);
884 * removes a directory
886 * @param string $path
887 * @param int $options
889 * @todo consider $options with STREAM_MKDIR_RECURSIVE
891 public function rmdir($path, $options)
893 $path = $this->resolvePath(vfsStream::path($path));
894 $child = $this->getContentOfType($path, vfsStreamContent::TYPE_DIR);
895 if (null === $child) {
899 // can only remove empty directories
900 if (count($child->getChildren()) > 0) {
904 if (self::$root->getName() === $path) {
905 // delete root? very brave. :)
911 $names = $this->splitPath($path);
912 $dir = $this->getContentOfType($names['dirname'], vfsStreamContent::TYPE_DIR);
913 if ($dir->isWritable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
918 return $dir->removeChild($child->getName());
924 * @param string $path
925 * @param int $options
928 public function dir_opendir($path, $options)
930 $path = $this->resolvePath(vfsStream::path($path));
931 $this->dir = $this->getContentOfType($path, vfsStreamContent::TYPE_DIR);
932 if (null === $this->dir || $this->dir->isReadable(vfsStream::getCurrentUser(), vfsStream::getCurrentGroup()) === false) {
936 $this->dirIterator = $this->dir->getIterator();
941 * reads directory contents
945 public function dir_readdir()
947 $dir = $this->dirIterator->current();
952 $this->dirIterator->next();
953 return $dir->getName();
957 * reset directory iteration
961 public function dir_rewinddir()
963 return $this->dirIterator->rewind();
971 public function dir_closedir()
973 $this->dirIterator = null;
978 * returns status of url
980 * @param string $path path of url to return status for
981 * @param int $flags flags set by the stream API
984 public function url_stat($path, $flags)
986 $content = $this->getContent($this->resolvePath(vfsStream::path($path)));
987 if (null === $content) {
988 if (($flags & STREAM_URL_STAT_QUIET) != STREAM_URL_STAT_QUIET) {
989 trigger_error(' No such file or directory: ' . $path, E_USER_WARNING);
996 $fileStat = array('dev' => 0,
998 'mode' => $content->getType() | $content->getPermissions(),
1000 'uid' => $content->getUser(),
1001 'gid' => $content->getGroup(),
1003 'size' => $content->size(),
1004 'atime' => $content->fileatime(),
1005 'mtime' => $content->filemtime(),
1006 'ctime' => $content->filectime(),
1010 return array_merge(array_values($fileStat), $fileStat);