├── tests ├── fixtures │ ├── css │ │ ├── reset.css │ │ ├── old │ │ │ └── old_style.css │ │ └── style.css │ ├── base.css │ ├── js │ │ └── script.js │ └── images │ │ ├── 1-top-left.jpg │ │ ├── 5-left-top.jpg │ │ ├── 2-top-right.jpg │ │ ├── 6-right-top.jpg │ │ ├── 3-bottom-right.jpg │ │ ├── 4-bottom-left.jpg │ │ ├── 7-right-bottom.jpg │ │ ├── 8-left-bottom.jpg │ │ └── nut.svg ├── fixtures2 │ └── empty.jpg ├── FilesystemTestCase.php ├── Handler │ ├── Image │ │ ├── ExifTest.php │ │ ├── TypeTest.php │ │ └── InfoTest.php │ ├── DirectoryTest.php │ └── FileTest.php ├── Iterator │ ├── CallbackMapIteratorTest.php │ ├── AppendIteratorTest.php │ ├── IteratorTestCase.php │ ├── FileContentFilterIteratorTest.php │ ├── RecursiveDirectoryIteratorTest.php │ ├── PathFilterIteratorTest.php │ ├── DateRangeFilterIteratorTest.php │ ├── ExcludeDirectoryFilterIteratorTest.php │ ├── SortableIteratorTest.php │ └── GlobIteratorTest.php └── Adapter │ └── LocalTest.php ├── .gitignore ├── src ├── Exception │ ├── NotSupportedException.php │ ├── PluginNotFoundException.php │ ├── RootViolationException.php │ ├── LogicException.php │ ├── RuntimeException.php │ ├── DumpException.php │ ├── BadMethodCallException.php │ ├── InvalidArgumentException.php │ ├── IncludeFileException.php │ ├── ExceptionInterface.php │ ├── FileExistsException.php │ ├── FileNotFoundException.php │ ├── DirectoryCreationException.php │ ├── IOException.php │ └── ParseException.php ├── Cached │ ├── Memory.php │ ├── Adapter.php │ ├── Psr6Cache.php │ ├── ImageInfoCacheTrait.php │ ├── PsrSimpleCache.php │ └── DoctrineCache.php ├── AggregateFilesystemInterface.php ├── SupportsIncludeFileInterface.php ├── Handler │ ├── ImageInterface.php │ ├── ParsableInterface.php │ ├── Image │ │ ├── SvgType.php │ │ ├── TypeInterface.php │ │ ├── Dimensions.php │ │ ├── Exif.php │ │ ├── CoreType.php │ │ ├── Type.php │ │ └── Info.php │ ├── JsonFile.php │ ├── Image.php │ ├── Directory.php │ ├── FileInterface.php │ ├── DirectoryInterface.php │ ├── YamlFile.php │ ├── File.php │ ├── HandlerInterface.php │ └── BaseHandler.php ├── PluginInterface.php ├── Capability │ ├── ImageInfo.php │ └── IncludeFile.php ├── MountPointAwareInterface.php ├── Iterator │ ├── AppendIterator.php │ ├── CallbackMapIterator.php │ ├── PathFilterIterator.php │ ├── DateRangeFilterIterator.php │ ├── EnsureHandlerIterator.php │ ├── FileContentFilterIterator.php │ ├── GlobIterator.php │ ├── SortableIterator.php │ ├── ExcludeDirectoryFilterIterator.php │ ├── MapIterator.php │ └── RecursiveDirectoryIterator.php ├── MountPointAwareTrait.php ├── Adapter │ ├── Sftp.php │ ├── Memory.php │ ├── Ftp.php │ ├── Cached.php │ ├── Local.php │ └── S3.php ├── ConfigAwareTrait.php ├── CompositeFilesystemInterface.php ├── Json.php ├── LazyFilesystem.php ├── Plugin │ └── PluggableTrait.php ├── FilesystemWrapperTrait.php └── FilesystemInterface.php ├── README.md ├── .travis.yml ├── phpunit.xml.dist └── composer.json /tests/fixtures/css/reset.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures2/empty.jpg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/fixtures/css/old/old_style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /test/temp 2 | /vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /tests/fixtures/base.css: -------------------------------------------------------------------------------- 1 | .koala { 2 | color: grey; 3 | width: 100% 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/css/style.css: -------------------------------------------------------------------------------- 1 | .koala { 2 | color: white; 3 | side: small; 4 | } 5 | -------------------------------------------------------------------------------- /tests/fixtures/js/script.js: -------------------------------------------------------------------------------- 1 | var BoltFilesystem = { 2 | console.debug('DROP BEAR ALERT!'); 3 | }; 4 | -------------------------------------------------------------------------------- /tests/fixtures/images/1-top-left.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/1-top-left.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/5-left-top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/5-left-top.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/2-top-right.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/2-top-right.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/6-right-top.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/6-right-top.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/3-bottom-right.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/3-bottom-right.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/4-bottom-left.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/4-bottom-left.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/7-right-bottom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/7-right-bottom.jpg -------------------------------------------------------------------------------- /tests/fixtures/images/8-left-bottom.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bolt/filesystem/HEAD/tests/fixtures/images/8-left-bottom.jpg -------------------------------------------------------------------------------- /src/Exception/NotSupportedException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IncludeFileException extends IOException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ExceptionInterface 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/Handler/ImageInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ImageInterface extends FileInterface 11 | { 12 | /** 13 | * Returns the info for this image. 14 | * 15 | * @return Image\Info 16 | */ 17 | public function getInfo(); 18 | } 19 | -------------------------------------------------------------------------------- /src/PluginInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface ImageInfo 14 | { 15 | /** 16 | * Return the info for an image. 17 | * 18 | * @param string $path The path to the image. 19 | * 20 | * @throws IOException 21 | * 22 | * @return Image\Info 23 | */ 24 | public function getImageInfo($path); 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: false 4 | dist: trusty 5 | 6 | php: 7 | - 5.5 8 | - 5.6 9 | - 7.0 10 | - 7.1 11 | - 7.2 12 | - hhvm 13 | 14 | matrix: 15 | fast_finish: true 16 | allow_failures: 17 | - php: hhvm 18 | 19 | before_install: 20 | 21 | before_script: 22 | # Set up Composer 23 | - composer self-update || true 24 | - composer install --prefer-dist 25 | 26 | script: 27 | # PHPUnit 28 | - vendor/bin/phpunit 29 | 30 | after_script: 31 | 32 | # Cache vendor dirs 33 | cache: 34 | directories: 35 | - vendor 36 | - $COMPOSER_CACHE_DIR 37 | 38 | -------------------------------------------------------------------------------- /src/MountPointAwareInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface MountPointAwareInterface 9 | { 10 | /** 11 | * Returns the aggregate filesystem's mount point. 12 | * 13 | * @return string|null 14 | */ 15 | public function getMountPoint(); 16 | 17 | /** 18 | * WARNING: Do not call this unless you know what you are doing. 19 | * 20 | * @param string $mountPoint 21 | * 22 | * @internal 23 | */ 24 | public function setMountPoint($mountPoint); 25 | } 26 | -------------------------------------------------------------------------------- /src/Exception/DirectoryCreationException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class DirectoryCreationException extends IOException 11 | { 12 | /** 13 | * Constructor. 14 | * 15 | * @param string $path 16 | * @param \Exception $previous 17 | */ 18 | public function __construct($path, \Exception $previous = null) 19 | { 20 | parent::__construct('Failed to create directory: ' . $path, $path, 0, $previous); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Iterator/AppendIterator.php: -------------------------------------------------------------------------------- 1 | getArrayIterator()->append($iterator); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | tests 13 | 14 | 15 | 16 | 17 | src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Handler/ParsableInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface ParsableInterface 11 | { 12 | /** 13 | * Read and parse the file's contents. 14 | * 15 | * @param array $options 16 | * 17 | * @return mixed 18 | */ 19 | public function parse($options = []); 20 | 21 | /** 22 | * Dump the data to the file. 23 | * 24 | * @param mixed $contents 25 | * @param array $options 26 | */ 27 | public function dump($contents, $options = []); 28 | } 29 | -------------------------------------------------------------------------------- /src/Capability/IncludeFile.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface IncludeFile 13 | { 14 | /** 15 | * Load a PHP file. 16 | * 17 | * @param string $path The file to include. 18 | * @param bool $once Whether to include the file only once. 19 | * 20 | * @throws IncludeFileException On failure. 21 | * 22 | * @return mixed Returns the return from the file or true if $once is true and this is a subsequent call. 23 | */ 24 | public function includeFile($path, $once = true); 25 | } 26 | -------------------------------------------------------------------------------- /src/MountPointAwareTrait.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | trait MountPointAwareTrait 9 | { 10 | /** @var string|null */ 11 | protected $mountPoint; 12 | 13 | /** 14 | * Returns the aggregate filesystem's mount point. 15 | * 16 | * @return string|null 17 | */ 18 | public function getMountPoint() 19 | { 20 | return $this->mountPoint; 21 | } 22 | 23 | /** 24 | * WARNING: Do not call this unless you know what you are doing. 25 | * 26 | * @param string $mountPoint 27 | */ 28 | public function setMountPoint($mountPoint) 29 | { 30 | $this->mountPoint = $mountPoint; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Adapter/Sftp.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Sftp extends SftpAdapter 14 | { 15 | /** 16 | * @inheritdoc 17 | */ 18 | public function createDir($dirname, Config $config) 19 | { 20 | if ($this->has($dirname)) { 21 | return true; 22 | } 23 | 24 | $connection = $this->getConnection(); 25 | if (!$connection->mkdir($dirname, $this->directoryPerm, true)) { 26 | return false; 27 | } 28 | // \phpseclib\Net\SFTP::mkdir() v2 fails to apply the correct 29 | // permissions on mkdir() when a umask of 022 is set, but chmod() still 30 | // works. 31 | $connection->chmod($this->directoryPerm, $dirname, true); 32 | 33 | return ['path' => $dirname]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exception/IOException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IOException extends RuntimeException 11 | { 12 | /** @var string|null */ 13 | private $path; 14 | 15 | /** 16 | * Constructor. 17 | * 18 | * @param string $message 19 | * @param string|null $path 20 | * @param int $code 21 | * @param \Exception|null $previous 22 | */ 23 | public function __construct($message, $path = null, $code = 0, \Exception $previous = null) 24 | { 25 | $this->path = $path; 26 | parent::__construct($message, $code, $previous); 27 | } 28 | 29 | /** 30 | * Returns the associated path for the exception. 31 | * 32 | * @return string|null 33 | */ 34 | public function getPath() 35 | { 36 | return $this->path; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Handler/Image/SvgType.php: -------------------------------------------------------------------------------- 1 | toString(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/FilesystemTestCase.php: -------------------------------------------------------------------------------- 1 | rootDir = __DIR__ . '/..'; 22 | $this->tempDir = $this->rootDir . '/tests/temp'; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function tearDown() 29 | { 30 | parent::tearDown(); 31 | 32 | $this->removeDirectory($this->tempDir); 33 | } 34 | 35 | protected function removeDirectory($dir) 36 | { 37 | if (!file_exists($dir)) { 38 | return; 39 | } 40 | 41 | $fs = new Symfony\Filesystem(); 42 | $fs->remove($dir); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Iterator/CallbackMapIterator.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class CallbackMapIterator extends MapIterator 13 | { 14 | /** @var callable */ 15 | protected $callback; 16 | 17 | /** 18 | * Constructor. 19 | * 20 | * @param array|Traversable $iterable The object to iterate. 21 | * @param callable $callback A callback which takes ($value, &$key) and returns the new value. 22 | */ 23 | public function __construct($iterable, callable $callback) 24 | { 25 | parent::__construct($iterable); 26 | $this->callback = $callback; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | protected function map($value, &$key) 33 | { 34 | $callback = $this->callback; 35 | // Don't use call_user_func, as the key won't be passed by reference 36 | return $callback($value, $key); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Handler/Image/TypeInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface TypeInterface 11 | { 12 | /** 13 | * Returns the ID. (probably IMAGETYPE_* constant). 14 | * 15 | * @return int 16 | */ 17 | public function getId(); 18 | 19 | /** 20 | * Returns the MIME Type associated with this type. 21 | * 22 | * @return string 23 | */ 24 | public function getMimeType(); 25 | 26 | /** 27 | * Returns the file extension for this type. 28 | * 29 | * @param bool $includeDot Whether to prepend a dot to the extension or not 30 | * 31 | * @return string 32 | */ 33 | public function getExtension($includeDot = true); 34 | 35 | /** 36 | * Returns the name of this type. 37 | * 38 | * @return string 39 | */ 40 | public function toString(); 41 | 42 | /** 43 | * Returns the name of this type. 44 | */ 45 | public function __toString(); 46 | } 47 | -------------------------------------------------------------------------------- /src/ConfigAwareTrait.php: -------------------------------------------------------------------------------- 1 | config = $config ? Util::ensureConfig($config) : new Config; 26 | } 27 | 28 | /** 29 | * Get the Config. 30 | * 31 | * @return Config config object 32 | */ 33 | public function getConfig() 34 | { 35 | return $this->config; 36 | } 37 | 38 | /** 39 | * Convert a config array to a Config object with the correct fallback. 40 | * 41 | * @param array $config 42 | * 43 | * @return Config 44 | */ 45 | protected function prepareConfig(array $config) 46 | { 47 | $config = new Config($config); 48 | $config->setFallback($this->getConfig()); 49 | 50 | return $config; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/CompositeFilesystemInterface.php: -------------------------------------------------------------------------------- 1 | Filesystem] 13 | */ 14 | public function mountFilesystems(array $filesystems); 15 | 16 | /** 17 | * Mount a filesystem. 18 | * 19 | * @param string $mountPoint 20 | * @param FilesystemInterface $filesystem 21 | */ 22 | public function mountFilesystem($mountPoint, FilesystemInterface $filesystem); 23 | 24 | /** 25 | * Get the filesystem at the given mount point. 26 | * 27 | * @param string $mountPoint 28 | * 29 | * @throws LogicException If the filesystem does not exist. 30 | * 31 | * @return FilesystemInterface 32 | */ 33 | public function getFilesystem($mountPoint); 34 | 35 | /** 36 | * Check if the filesystem at the given mount point exists. 37 | * 38 | * @param string $mountPoint 39 | * 40 | * @return bool 41 | */ 42 | public function hasFilesystem($mountPoint); 43 | } 44 | -------------------------------------------------------------------------------- /src/Iterator/PathFilterIterator.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PathFilterIterator extends PathFilterIteratorBase 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function accept() 19 | { 20 | /** @var File $file */ 21 | $file = $this->current(); 22 | 23 | $filename = $file->getPath(); 24 | 25 | // should at least not match one rule to exclude 26 | foreach ($this->noMatchRegexps as $regex) { 27 | if (preg_match($regex, $filename)) { 28 | return false; 29 | } 30 | } 31 | 32 | // should at least match one rule 33 | $match = true; 34 | if ($this->matchRegexps) { 35 | $match = false; 36 | foreach ($this->matchRegexps as $regex) { 37 | if (preg_match($regex, $filename)) { 38 | return true; 39 | } 40 | } 41 | } 42 | 43 | return $match; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Handler/Image/ExifTest.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ExifTest extends TestCase 15 | { 16 | public function testConstruct() 17 | { 18 | $exif = new Exif([]); 19 | $this->assertInstanceOf('Bolt\Filesystem\Handler\Image\Exif', $exif); 20 | } 21 | 22 | public function testCast() 23 | { 24 | $exif = new Exif([]); 25 | $this->assertInstanceOf('Bolt\Filesystem\Handler\Image\Exif', $exif->cast(new PHPExif\Exif([]))); 26 | } 27 | 28 | public function testInvalidGps() 29 | { 30 | $exif = new Exif([]); 31 | $this->assertFalse($exif->getLatitude()); 32 | } 33 | 34 | public function testGetLatitude() 35 | { 36 | $exif = new Exif([Exif::GPS => '35.25513,149.1093073']); 37 | $this->assertSame(35.25513, $exif->getLatitude()); 38 | } 39 | 40 | public function testGetLongitude() 41 | { 42 | $exif = new Exif([Exif::GPS => '35.25513,149.1093073']); 43 | $this->assertSame(149.1093073, $exif->getLongitude()); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Cached/ImageInfoCacheTrait.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | trait ImageInfoCacheTrait 13 | { 14 | /** 15 | * @param string $path 16 | * 17 | * @return Image\Info|array|false 18 | */ 19 | public function getImageInfo($path) 20 | { 21 | if (isset($this->cache[$path]['image_info'])) { 22 | return $this->cache[$path]['image_info']; 23 | } 24 | 25 | return false; 26 | } 27 | 28 | public function cleanContents(array $contents) 29 | { 30 | $cachedProperties = array_flip($this->getPersistedProperties()); 31 | 32 | foreach ($contents as $path => $object) { 33 | if (is_array($object)) { 34 | $contents[$path] = array_intersect_key($object, $cachedProperties); 35 | } 36 | } 37 | 38 | return $contents; 39 | } 40 | 41 | protected function getPersistedProperties() 42 | { 43 | return [ 44 | 'path', 'dirname', 'basename', 'extension', 'filename', 45 | 'size', 'mimetype', 'visibility', 'timestamp', 'type', 46 | 'image_info', 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Adapter/Memory.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class Memory extends MemoryAdapter implements Capability\IncludeFile 16 | { 17 | private $includedFiles = []; 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function includeFile($path, $once = true) 23 | { 24 | if ($once && isset($this->includedFiles[$path])) { 25 | return true; 26 | } 27 | 28 | $contents = $this->read($path)['contents']; 29 | try { 30 | $contents = Thrower::call(__NAMESPACE__ . '\evalContents', $contents); 31 | } catch (\Exception $e) { 32 | throw new IncludeFileException($e->getMessage(), $path, 0, $e); 33 | } 34 | 35 | $this->includedFiles[$path] = true; 36 | 37 | return $contents; 38 | } 39 | } 40 | 41 | /** 42 | * Scope isolated include. 43 | * 44 | * Prevents access to $this/self from included files. 45 | * 46 | * @param string $__data 47 | * 48 | * @return mixed 49 | */ 50 | function evalContents($__data) 51 | { 52 | return eval('?>' . $__data); 53 | } 54 | -------------------------------------------------------------------------------- /src/Cached/PsrSimpleCache.php: -------------------------------------------------------------------------------- 1 | cache = $cache; 30 | $this->key = $key; 31 | $this->expire = $expire; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function save() 38 | { 39 | $this->cache->set($this->key, $this->getForStorage(), $this->expire); 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function load() 46 | { 47 | if ($value = $this->cache->get($this->key)) { 48 | $this->setFromStorage($value); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Cached/DoctrineCache.php: -------------------------------------------------------------------------------- 1 | doctrine = $cache; 30 | $this->key = $key; 31 | $this->lifeTime = $lifeTime; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function save() 38 | { 39 | $contents = $this->getForStorage(); 40 | $this->doctrine->save($this->key, $contents, $this->lifeTime); 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function load() 47 | { 48 | $contents = $this->doctrine->fetch($this->key); 49 | if ($contents !== false) { 50 | $this->setFromStorage($contents); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Iterator/DateRangeFilterIterator.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Carson Full 14 | */ 15 | class DateRangeFilterIterator extends \FilterIterator 16 | { 17 | private $comparators = []; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param \Iterator $iterator The Iterator to filter 23 | * @param DateComparator[] $comparators An array of DateComparator instances 24 | */ 25 | public function __construct(\Iterator $iterator, array $comparators) 26 | { 27 | $this->comparators = $comparators; 28 | 29 | parent::__construct($iterator); 30 | } 31 | 32 | public function accept() 33 | { 34 | /** @var Directory|File $handler */ 35 | $handler = $this->current(); 36 | 37 | if (!$handler->exists()) { 38 | return false; 39 | } 40 | 41 | $timestamp = $handler->getTimestamp(); 42 | foreach ($this->comparators as $compare) { 43 | if (!$compare->test($timestamp)) { 44 | return false; 45 | } 46 | } 47 | 48 | return true; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Handler/JsonFile.php: -------------------------------------------------------------------------------- 1 | 13 | * @author Carson Full 14 | */ 15 | class JsonFile extends File implements ParsableInterface 16 | { 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function parse($options = []) 21 | { 22 | $options += [ 23 | 'depth' => 512, 24 | 'flags' => 0, 25 | ]; 26 | 27 | $contents = $this->read(); 28 | 29 | try { 30 | return Json::parse($contents, $options['flags'], $options['depth']); 31 | } catch (\Bolt\Common\Exception\ParseException $e) { 32 | throw new ParseException($e->getRawMessage(), $e->getParsedLine(), $e->getSnippet(), $e); 33 | } 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function dump($contents, $options = []) 40 | { 41 | $options += [ 42 | 'flags' => 448, 43 | ]; 44 | 45 | try { 46 | $content = Json::dump($contents, $options['flags']); 47 | } catch (\Bolt\Common\Exception\DumpException $e) { 48 | throw new DumpException($e->getMessage(), $e->getCode(), $e); 49 | } 50 | 51 | $this->put($content); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Iterator/EnsureHandlerIterator.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class EnsureHandlerIterator extends MapIterator 17 | { 18 | /** @var FilesystemInterface */ 19 | private $filesystem; 20 | 21 | /** 22 | * Constructor. 23 | * 24 | * @param FilesystemInterface $filesystem 25 | * @param array|Traversable $iterable 26 | */ 27 | public function __construct(FilesystemInterface $filesystem, $iterable) 28 | { 29 | parent::__construct($iterable); 30 | $this->filesystem = $filesystem; 31 | } 32 | 33 | /** 34 | * {@inheritdoc} 35 | */ 36 | protected function map($value, &$key) 37 | { 38 | if ($value instanceof HandlerInterface) { 39 | return $value; 40 | } 41 | if (is_string($value)) { 42 | return $this->filesystem->get($value); 43 | } 44 | 45 | throw new InvalidArgumentException(sprintf('Iterators or arrays given to Finder::append() must give path strings or %s objects.', HandlerInterface::class)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/Iterator/CallbackMapIteratorTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ArrayIterator::class, $it->getInnerIterator()); 24 | 25 | $expected = [ 26 | 'foo' => 'foo.', 27 | 'bar' => 'bar.', 28 | ]; 29 | $this->assertEquals($expected, $it->toArray()); 30 | $this->assertEquals($expected, $it->toArray(), 'Should be able to be iterated multiple times'); 31 | } 32 | 33 | public function testIterator() 34 | { 35 | $input = new ArrayIterator([ 36 | 'foo', 37 | 'bar', 38 | ]); 39 | $it = new CallbackMapIterator($input, function ($item) { 40 | return $item . '.'; 41 | }); 42 | 43 | $this->assertEquals(['foo.', 'bar.'], $it->toArray()); 44 | } 45 | 46 | /** 47 | * @expectedException \InvalidArgumentException 48 | */ 49 | public function testNonIterable() 50 | { 51 | new CallbackMapIterator(new \stdClass(), 'var_dump'); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Iterator/FileContentFilterIterator.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class FileContentFilterIterator extends FilecontentFilterIteratorBase 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function accept() 20 | { 21 | if (!$this->matchRegexps && !$this->noMatchRegexps) { 22 | return true; 23 | } 24 | 25 | /** @var File $handler */ 26 | $handler = $this->current(); 27 | 28 | if (!$handler->isFile()) { 29 | return false; 30 | } 31 | 32 | try { 33 | $content = $handler->read(); 34 | } catch (IOException $e) { 35 | return false; 36 | } 37 | 38 | // should at least not matach one rule to exclude 39 | foreach ($this->noMatchRegexps as $regex) { 40 | if (preg_match($regex, $content)) { 41 | return false; 42 | } 43 | } 44 | 45 | // should at least match one rule 46 | $match = true; 47 | if ($this->matchRegexps) { 48 | $match = false; 49 | foreach ($this->matchRegexps as $regex) { 50 | if (preg_match($regex, $content)) { 51 | return true; 52 | } 53 | } 54 | } 55 | 56 | return $match; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Adapter/Ftp.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class Ftp extends FtpAdapter 14 | { 15 | /** @var int */ 16 | protected $directoryPerm = 0744; 17 | 18 | /** 19 | * @inheritdoc 20 | */ 21 | public function __construct(array $config) 22 | { 23 | $this->configurable[] = 'directoryPerm'; 24 | parent::__construct($config); 25 | } 26 | 27 | /** 28 | * @return int 29 | */ 30 | public function getDirectoryPerm() 31 | { 32 | return $this->directoryPerm; 33 | } 34 | 35 | /** 36 | * @param int $directoryPerm 37 | * 38 | * @return Ftp 39 | */ 40 | public function setDirectoryPerm($directoryPerm) 41 | { 42 | $this->directoryPerm = $directoryPerm; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * @inheritdoc 49 | */ 50 | public function createDir($dirname, Config $config) 51 | { 52 | if ($this->has($dirname)) { 53 | return true; 54 | } 55 | 56 | return parent::createDir($dirname, $config); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | protected function createActualDirectory($directory, $connection) 63 | { 64 | $result = parent::createActualDirectory($directory, $connection); 65 | if ($result) { 66 | ftp_chmod($connection, $this->directoryPerm, $directory); 67 | } 68 | 69 | return $result; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Handler/Image.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Image extends File implements ImageInterface 13 | { 14 | /** @var Image\Info */ 15 | protected $info; 16 | 17 | /** 18 | * {@inheritdoc} 19 | */ 20 | public function getInfo($cache = true) 21 | { 22 | if (!$cache) { 23 | $this->info = null; 24 | } 25 | if (!$this->info) { 26 | $this->info = $this->filesystem->getImageInfo($this->path); 27 | } 28 | 29 | return $this->info; 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | * 35 | * Use MIME Type from Info as it has handles SVG detection better. 36 | */ 37 | public function getMimeType() 38 | { 39 | return $this->getInfo()->getMime(); 40 | } 41 | 42 | /** 43 | * Pass-through to plugins, then Image\Info. This is for BC. 44 | * 45 | * @param string $method 46 | * @param array $arguments 47 | * 48 | * @return mixed 49 | */ 50 | public function __call($method, array $arguments) 51 | { 52 | try { 53 | return parent::__call($method, $arguments); 54 | } catch (BadMethodCallException $e) { 55 | } 56 | 57 | $info = $this->getInfo(); 58 | if (method_exists($info, 'get' . $method)) { 59 | return call_user_func([$info, 'get' . $method]); 60 | } elseif (method_exists($info, 'is' . $method)) { 61 | return call_user_func([$info, 'is' . $method]); 62 | } 63 | 64 | throw $e; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bolt/filesystem", 3 | "description": "Bolt's filesystem abstraction layer", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Carson Full", 8 | "email": "carsonfull@gmail.com" 9 | }, 10 | { 11 | "name": "Gawain Lynch", 12 | "email": "gawain.lynch@gmail.com" 13 | } 14 | ], 15 | "require": { 16 | "bolt/common": "^1.0", 17 | "ext-json": "*", 18 | "guzzlehttp/psr7": "^1.2", 19 | "league/flysystem": "^1.0", 20 | "miljar/php-exif": "^0.6", 21 | "nesbot/carbon": "^1.20", 22 | "php": "^5.5.9 || ^7.0", 23 | "webmozart/glob": "^4.0", 24 | "symfony/finder": "^2.7 || ^3.0 || ^4.0", 25 | "symfony/yaml": "^2.7 || ^3.0 || ^4.0" 26 | }, 27 | "require-dev": { 28 | "contao/imagine-svg": "^0.1.2", 29 | "doctrine/cache": "^1.6", 30 | "league/flysystem-aws-s3-v3": "^1.0", 31 | "league/flysystem-cached-adapter": "^1.0", 32 | "league/flysystem-memory": "^1.0", 33 | "league/flysystem-sftp": "^1.0", 34 | "phpunit/phpunit": "^4.8.36 || ^5.0 || ^6.0", 35 | "psr/simple-cache": "^1.0", 36 | "symfony/filesystem": "^2.7 || ^3.0 || ^4.0" 37 | }, 38 | "suggest": { 39 | "contao/imagine-svg": "To parse SVG image info" 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Bolt\\Filesystem\\": "src" 44 | } 45 | }, 46 | "autoload-dev": { 47 | "psr-4": { 48 | "Bolt\\Filesystem\\Tests\\": "tests" 49 | } 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "2.x-dev" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Iterator/AppendIteratorTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('Bug does not exist on HHVM'); 19 | } 20 | 21 | $this->assertSequence(new \AppendIterator(), '..123.456'); 22 | } 23 | 24 | /** 25 | * This asserts that our AppendIterator fixes it. 26 | */ 27 | public function testFix() 28 | { 29 | $this->assertSequence(new AppendIterator(), '.123.456'); 30 | } 31 | 32 | private function assertSequence(\AppendIterator $it, $sequence) 33 | { 34 | $str = new TestStr(); 35 | $i1 = new TestArrayIterator([1, 2, 3], $str); 36 | $i2 = new TestArrayIterator([4, 5, 6], $str); 37 | 38 | $it->append($i1); 39 | $it->append($i2); 40 | 41 | foreach ($it as $item) { 42 | $str->str .= $item; 43 | } 44 | $this->assertSame($sequence, $str->str); 45 | } 46 | } 47 | 48 | class TestArrayIterator extends ArrayIterator 49 | { 50 | private $str; 51 | 52 | public function __construct(array $array, TestStr $str) 53 | { 54 | parent::__construct($array); 55 | $this->str = $str; 56 | } 57 | 58 | public function rewind() 59 | { 60 | $this->str->str .= '.'; 61 | parent::rewind(); 62 | } 63 | } 64 | 65 | class TestStr 66 | { 67 | public $str = ''; 68 | } 69 | -------------------------------------------------------------------------------- /src/Iterator/GlobIterator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class GlobIterator extends GlobFilterIterator 16 | { 17 | /** 18 | * Constructor. 19 | * 20 | * @param FilesystemInterface $filesystem The filesystem to search 21 | * @param string $glob The glob pattern 22 | * @param int $flags A bitwise combination of the flag constants in {@see Glob} 23 | */ 24 | public function __construct(FilesystemInterface $filesystem, $glob, $flags = 0) 25 | { 26 | // Glob code requires absolute paths, so prefix path 27 | // with leading slash, but not before mount point 28 | if (strpos($glob, '://') > 0) { 29 | $glob = str_replace('://', ':///', $glob); 30 | } else { 31 | $glob = '/' . ltrim($glob, '/'); 32 | } 33 | 34 | if (!Glob::isDynamic($glob)) { 35 | // If the glob is a file path, return that path. 36 | $innerIterator = new \ArrayIterator([$glob => $filesystem->get($glob)]); 37 | } else { 38 | $basePath = Glob::getBasePath($glob); 39 | 40 | $innerIterator = new RecursiveIteratorIterator( 41 | new RecursiveDirectoryIterator( 42 | $filesystem, 43 | $basePath, 44 | RecursiveDirectoryIterator::KEY_FOR_GLOB 45 | ), 46 | RecursiveIteratorIterator::SELF_FIRST 47 | ); 48 | } 49 | 50 | parent::__construct($glob, $innerIterator, static::FILTER_KEY, $flags); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/Iterator/IteratorTestCase.php: -------------------------------------------------------------------------------- 1 | getPath(); 16 | }, 17 | iterator_to_array($iterator) 18 | ); 19 | 20 | sort($values); 21 | sort($expected); 22 | 23 | $this->assertEquals($expected, array_values($values)); 24 | 25 | $this->assertIteratorInForeach($expected, $iterator); 26 | } 27 | 28 | protected function assertOrderedIterator($expected, Traversable $iterator) 29 | { 30 | $values = array_map( 31 | function (HandlerInterface $handler) { 32 | return $handler->getPath(); 33 | }, 34 | iterator_to_array($iterator) 35 | ); 36 | 37 | $this->assertEquals($expected, array_values($values)); 38 | } 39 | 40 | /** 41 | * Same as IteratorTestCase::assertIterator with foreach usage. 42 | * 43 | * @param array $expected 44 | * @param Traversable $iterator 45 | */ 46 | protected function assertIteratorInForeach($expected, Traversable $iterator) 47 | { 48 | $values = []; 49 | foreach ($iterator as $handler) { 50 | /** @var HandlerInterface $handler */ 51 | $this->assertInstanceOf(HandlerInterface::class, $handler); 52 | $values[] = $handler->getPath(); 53 | } 54 | 55 | sort($values); 56 | sort($expected); 57 | 58 | $this->assertEquals($expected, array_values($values)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/Handler/DirectoryTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DirectoryTest extends FilesystemTestCase 19 | { 20 | /** @var FilesystemInterface */ 21 | protected $filesystem; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function setUp() 27 | { 28 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../')); 29 | } 30 | 31 | public function testConstruct() 32 | { 33 | $dir = new Directory($this->filesystem); 34 | $this->assertInstanceOf(Directory::class, $dir); 35 | 36 | $filesystem = new Filesystem(new Local(__DIR__)); 37 | $dir = new Directory($filesystem); 38 | $this->assertInstanceOf(Directory::class, $dir); 39 | } 40 | 41 | public function testSetFilesystem() 42 | { 43 | $dir = new Directory($this->filesystem); 44 | $filesystem = new Filesystem(new Local(__DIR__)); 45 | $dir->setFilesystem($filesystem); 46 | $this->assertInstanceOf(Filesystem::class, $dir->getFilesystem()); 47 | } 48 | 49 | public function testGet() 50 | { 51 | $dir = new Directory($this->filesystem); 52 | $this->assertInstanceOf(File::class, $dir->get('fixtures/base.css')); 53 | } 54 | 55 | public function testGetContents() 56 | { 57 | $dir = new Directory($this->filesystem); 58 | $content = $dir->getContents(); 59 | $this->assertInstanceOf(HandlerInterface::class, $content[0]); 60 | } 61 | 62 | public function testExists() 63 | { 64 | $dir = new Directory($this->filesystem); 65 | $this->assertTrue($dir->exists()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Exception/ParseException.php: -------------------------------------------------------------------------------- 1 | getCode() : 0, $previous); 20 | } 21 | 22 | /** 23 | * Casts Symfony's Yaml ParseException to ours. 24 | * 25 | * @param YamlParseException $exception 26 | * 27 | * @return ParseException 28 | */ 29 | public static function castFromYaml(YamlParseException $exception) 30 | { 31 | $message = static::parseRawMessage($exception->getMessage()); 32 | 33 | return new static($message, $exception->getParsedLine(), $exception->getSnippet(), $exception); 34 | } 35 | 36 | /** 37 | * Parse the raw message from Symfony's Yaml ParseException 38 | * 39 | * @param string $message 40 | * 41 | * @return string 42 | */ 43 | private static function parseRawMessage($message) 44 | { 45 | $dot = false; 46 | if (substr($message, -1) === '.') { 47 | $message = substr($message, 0, -1); 48 | $dot = true; 49 | } 50 | 51 | if (($pos = strpos($message, ' at line')) > 0) { 52 | $message = substr($message, 0, $pos); 53 | } elseif (($pos = strpos($message, ' (near')) > 0) { 54 | $message = substr($message, 0, $pos); 55 | } 56 | 57 | if ($dot) { 58 | $message .= '.'; 59 | } 60 | 61 | return $message; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Json.php: -------------------------------------------------------------------------------- 1 | getMessage(), $e->getCode(), $e); 37 | } 38 | } 39 | 40 | /** 41 | * Dumps a array/object into a JSON string. 42 | * 43 | * @param mixed $data Data to encode into a formatted JSON string 44 | * @param int $options json_encode options 45 | * (defaults to JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) 46 | * 47 | * @throws DumpException If dumping fails 48 | * 49 | * @return string 50 | * 51 | * @deprecated since 2.4 and will be removed in 3.0. Use {@see \Bolt\Common\Json::dump} instead. 52 | */ 53 | public static function dump($data, $options = 448) 54 | { 55 | Deprecated::method(2.4, \Bolt\Common\Json::class . '::dump'); 56 | 57 | try { 58 | return \Bolt\Common\Json::dump($data, $options); 59 | } catch (\Bolt\Common\Exception\ParseException $e) { 60 | throw new ParseException($e->getRawMessage(), $e->getParsedLine(), $e->getSnippet(), $e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Handler/Image/Dimensions.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Dimensions 13 | { 14 | /** @var int */ 15 | protected $width; 16 | /** @var int */ 17 | protected $height; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param int $width The width 23 | * @param int $height The height 24 | */ 25 | public function __construct($width = 0, $height = 0) 26 | { 27 | $this->setWidth($width); 28 | $this->setHeight($height); 29 | } 30 | 31 | /** 32 | * Returns the width. 33 | * 34 | * @return int 35 | */ 36 | public function getWidth() 37 | { 38 | return $this->width; 39 | } 40 | 41 | /** 42 | * Sets the width. 43 | * 44 | * @param int $width 45 | * 46 | * @return Dimensions 47 | */ 48 | public function setWidth($width) 49 | { 50 | $this->verify($width); 51 | $this->width = (int) $width; 52 | 53 | return $this; 54 | } 55 | 56 | /** 57 | * Returns the height. 58 | * 59 | * @return int 60 | */ 61 | public function getHeight() 62 | { 63 | return $this->height; 64 | } 65 | 66 | /** 67 | * Sets the height. 68 | * 69 | * @param int $height 70 | * 71 | * @return Dimensions 72 | */ 73 | public function setHeight($height) 74 | { 75 | $this->verify($height); 76 | $this->height = (int) $height; 77 | 78 | return $this; 79 | } 80 | 81 | /** 82 | * @inheritDoc 83 | */ 84 | public function __toString() 85 | { 86 | return $this->width . ' × ' . $this->height . ' px'; 87 | } 88 | 89 | /** 90 | * Verifies that the dimension is valid. 91 | * 92 | * @param int|mixed $point 93 | */ 94 | protected function verify($point) 95 | { 96 | if (!is_numeric($point)) { 97 | throw new InvalidArgumentException('Dimensions point is expected to be numeric'); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /tests/fixtures/images/nut.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/Handler/Image/Exif.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Exif extends PHPExif\Exif 13 | { 14 | /** 15 | * Casts Exif to this sub-class. 16 | * 17 | * @param PHPExif\Exif $exif 18 | * 19 | * @return Exif 20 | */ 21 | public static function cast(PHPExif\Exif $exif) 22 | { 23 | $new = new static($exif->getData()); 24 | $new->setRawData($exif->getRawData()); 25 | 26 | return $new; 27 | } 28 | 29 | /** 30 | * Returns the aspect ratio. 31 | * 32 | * @return float 33 | */ 34 | public function getAspectRatio() 35 | { 36 | if ($this->getWidth() == 0 || $this->getHeight() == 0) { 37 | return 0.0; 38 | } 39 | 40 | // Account for image rotation 41 | if (in_array($this->getOrientation(), [5, 6, 7, 8])) { 42 | return $this->getHeight() / $this->getWidth(); 43 | } 44 | 45 | return $this->getWidth() / $this->getHeight(); 46 | } 47 | 48 | /** 49 | * Returns the latitude from the GPS data, if it exists. 50 | * 51 | * @return bool|float 52 | */ 53 | public function getLatitude() 54 | { 55 | return $this->getGpsPart(0); 56 | } 57 | 58 | /** 59 | * Returns the longitude from the GPS data, if it exists. 60 | * 61 | * @return bool|float 62 | */ 63 | public function getLongitude() 64 | { 65 | return $this->getGpsPart(1); 66 | } 67 | 68 | /** 69 | * @param $index 70 | * 71 | * @return bool|float 72 | */ 73 | private function getGpsPart($index) 74 | { 75 | $gps = $this->getGPS(); 76 | if ($gps === false) { 77 | return false; 78 | } 79 | 80 | $parts = explode(',', $gps); 81 | if (!isset($parts[$index])) { 82 | return false; 83 | } 84 | 85 | return (float) $parts[$index]; 86 | } 87 | 88 | /** 89 | * Returns the creation datetime, if it exists. 90 | * 91 | * @deprecated Use {@see Exif::getCreationDate} instead. 92 | * 93 | * @return bool|\DateTime 94 | */ 95 | public function getDateTime() 96 | { 97 | return $this->getCreationDate(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Handler/Directory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class Directory extends BaseHandler implements DirectoryInterface 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | public function isRoot() 16 | { 17 | return $this->path === ''; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function create($config = []) 24 | { 25 | $this->filesystem->createDir($this->path, $config); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function delete() 32 | { 33 | $this->filesystem->deleteDir($this->path); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function copy($target, $override = null) 40 | { 41 | $this->filesystem->copyDir($this->path, $target, $override); 42 | 43 | return new static($this->filesystem, $target); 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function mirror($target, $config = []) 50 | { 51 | $this->filesystem->mirror($this->path, $target, $config); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function get($path, HandlerInterface $handler = null) 58 | { 59 | return $this->filesystem->get($this->path . '/' . $path, $handler); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getFile($path, FileInterface $handler = null) 66 | { 67 | return $this->filesystem->getFile($this->path . '/' . $path, $handler); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function getDir($path) 74 | { 75 | return $this->filesystem->getDir($this->path . '/' . $path); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getImage($path) 82 | { 83 | return $this->filesystem->getImage($this->path . '/' . $path); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function getContents($recursive = false) 90 | { 91 | return $this->filesystem->listContents($this->path, $recursive); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function find() 98 | { 99 | return $this->filesystem->find()->in($this->path); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Iterator/SortableIterator.php: -------------------------------------------------------------------------------- 1 | 14 | * @author Carson Full 15 | */ 16 | class SortableIterator implements \IteratorAggregate 17 | { 18 | const SORT_BY_NAME = 1; 19 | const SORT_BY_TYPE = 2; 20 | const SORT_BY_TIME = 3; 21 | 22 | private $iterator; 23 | private $sort; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @param \Traversable $iterator The Iterator to filter 29 | * @param int|callable $sort The sort type (SORT_BY_NAME, SORT_BY_TYPE, or a PHP callback) 30 | * 31 | * @throws InvalidArgumentException 32 | */ 33 | public function __construct(\Traversable $iterator, $sort) 34 | { 35 | $this->iterator = $iterator; 36 | 37 | if (self::SORT_BY_NAME === $sort) { 38 | $this->sort = function (HandlerInterface $a, HandlerInterface $b) { 39 | return strcmp($a->getPath(), $b->getPath()); 40 | }; 41 | } elseif (self::SORT_BY_TYPE === $sort) { 42 | $this->sort = function (HandlerInterface $a, HandlerInterface $b) { 43 | if ($a->isDir() && $b->isFile()) { 44 | return -1; 45 | } elseif ($a->isFile() && $b->isDir()) { 46 | return 1; 47 | } 48 | 49 | return strcmp($a->getPath(), $b->getPath()); 50 | }; 51 | } elseif (self::SORT_BY_TIME === $sort) { 52 | $this->sort = function ($a, $b) { 53 | /** @var File|Directory $a */ 54 | /** @var File|Directory $b */ 55 | return $a->getTimestamp() - $b->getTimestamp(); 56 | }; 57 | } elseif (is_callable($sort)) { 58 | $this->sort = $sort; 59 | } else { 60 | throw new InvalidArgumentException('The SortableIterator takes a PHP callable or a valid built-in sort algorithm as an argument.'); 61 | } 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getIterator() 68 | { 69 | $array = iterator_to_array($this->iterator, true); 70 | uasort($array, $this->sort); 71 | 72 | return new \ArrayIterator($array); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Iterator/ExcludeDirectoryFilterIterator.php: -------------------------------------------------------------------------------- 1 | iterator = $iterator; 25 | $this->isRecursive = $iterator instanceof \RecursiveIterator; 26 | $patterns = []; 27 | foreach ($directories as $directory) { 28 | if (!$this->isRecursive || false !== strpos($directory, '/')) { 29 | $patterns[] = preg_quote($directory, '#'); 30 | } else { 31 | $this->excludedDirs[$directory] = true; 32 | } 33 | } 34 | if ($patterns) { 35 | $this->excludedPattern = '#(?:^|/)(?:' . implode('|', $patterns) . ')(?:/|$)#'; 36 | } 37 | 38 | parent::__construct($iterator); 39 | } 40 | 41 | /** 42 | * Filters the iterator values. 43 | * 44 | * @return bool true if the value should be kept, false otherwise 45 | */ 46 | public function accept() 47 | { 48 | /** @var Directory|File $handler */ 49 | $handler = $this->iterator->current(); 50 | if ($this->isRecursive && isset($this->excludedDirs[$handler->getFilename()]) && $handler->isDir()) { 51 | return false; 52 | } 53 | 54 | if ($this->excludedPattern) { 55 | $path = $handler->isDir() ? $handler->getPath() : $handler->getDirname(); 56 | $path = str_replace('\\', '/', $path); 57 | 58 | return !preg_match($this->excludedPattern, $path); 59 | } 60 | 61 | return true; 62 | } 63 | 64 | public function hasChildren() 65 | { 66 | return $this->isRecursive && $this->iterator->hasChildren(); 67 | } 68 | 69 | public function getChildren() 70 | { 71 | $children = new self($this->iterator->getChildren(), []); 72 | $children->excludedDirs = $this->excludedDirs; 73 | $children->excludedPattern = $this->excludedPattern; 74 | 75 | return $children; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Handler/Image/TypeTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class TypeTest extends TestCase 16 | { 17 | /** 18 | * @expectedException \Bolt\Filesystem\Exception\InvalidArgumentException 19 | * @expectedExceptionMessage Given type is not an IMAGETYPE_* constant 20 | */ 21 | public function testGetById() 22 | { 23 | $type = Type::getById(IMAGETYPE_JPEG); 24 | $this->assertInstanceOf(TypeInterface::class, $type); 25 | 26 | $type2 = Type::getById(IMAGETYPE_JPEG); 27 | $this->assertSame($type, $type2); 28 | 29 | Type::getById(42); 30 | } 31 | 32 | public function testToId() 33 | { 34 | $type = Type::getById(IMAGETYPE_JPEG); 35 | $this->assertSame(2, $type->getId()); 36 | } 37 | 38 | public function testToMimeType() 39 | { 40 | $type = Type::getById(IMAGETYPE_JPEG); 41 | $this->assertSame('image/jpeg', $type->getMimeType()); 42 | } 43 | 44 | public function testToExtension() 45 | { 46 | $type = Type::getById(IMAGETYPE_JPEG); 47 | $this->assertSame('.jpeg', $type->getExtension(true)); 48 | $this->assertSame('jpeg', $type->getExtension(false)); 49 | } 50 | 51 | public function testToString() 52 | { 53 | $type = Type::getById(IMAGETYPE_JPEG); 54 | $this->assertSame('JPEG', $type->toString()); 55 | $this->assertSame('JPEG', (string) $type); 56 | } 57 | 58 | public function testSvg() 59 | { 60 | $type = Type::getById(SvgType::ID); 61 | $this->assertEquals(101, $type->getId()); 62 | $this->assertEquals('image/svg+xml', $type->getMimeType()); 63 | $this->assertEquals('.svg', $type->getExtension()); 64 | $this->assertEquals('svg', $type->getExtension(false)); 65 | $this->assertEquals('SVG', $type->toString()); 66 | $this->assertEquals('SVG', (string) $type); 67 | } 68 | 69 | public function testGetTypes() 70 | { 71 | $types = Type::getTypes(); 72 | $this->assertInstanceOf(TypeInterface::class, $types[0]); 73 | } 74 | 75 | public function testGetMimeTypes() 76 | { 77 | $mimeTypes = Type::getMimeTypes(); 78 | $this->assertContains('image/jpeg', $mimeTypes); 79 | } 80 | 81 | public function testGetExtensions() 82 | { 83 | $extensions = Type::getExtensions(); 84 | $this->assertContains('jpeg', $extensions); 85 | $this->assertContains('jpg', $extensions); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/LazyFilesystem.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class LazyFilesystem implements FilesystemInterface, MountPointAwareInterface 13 | { 14 | use FilesystemWrapperTrait; 15 | 16 | /** @var callable */ 17 | protected $factory; 18 | /** @var FilesystemInterface|null */ 19 | protected $filesystem; 20 | /** @var string|null */ 21 | protected $mountPoint; 22 | /** @var PluginInterface[] */ 23 | protected $plugins = []; 24 | 25 | /** 26 | * Constructor. 27 | * 28 | * @param callable $factory This callable must return a FilesystemInterface when called. 29 | */ 30 | public function __construct(callable $factory) 31 | { 32 | $this->factory = $factory; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function wrapped() 39 | { 40 | if (!$this->filesystem) { 41 | $this->filesystem = call_user_func($this->factory); 42 | if (!$this->filesystem instanceof FilesystemInterface) { 43 | throw new LogicException('Factory supplied to LazyFilesystem must return an implementation of FilesystemInterface'); 44 | } 45 | 46 | if ($this->filesystem instanceof MountPointAwareInterface) { 47 | $this->filesystem->setMountPoint($this->mountPoint); 48 | $this->mountPoint = null; 49 | } 50 | 51 | foreach ($this->plugins as $plugin) { 52 | $this->filesystem->addPlugin($plugin); 53 | } 54 | $this->plugins = []; 55 | } 56 | 57 | return $this->filesystem; 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | * 63 | * Plugins are added lazily. 64 | */ 65 | public function addPlugin(PluginInterface $plugin) 66 | { 67 | if ($this->filesystem) { 68 | $this->filesystem->addPlugin($plugin); 69 | } else { 70 | $this->plugins[] = $plugin; 71 | } 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function getMountPoint() 78 | { 79 | $filesystem = $this->wrapped(); 80 | 81 | if ($filesystem instanceof MountPointAwareInterface) { 82 | return $filesystem->getMountPoint(); 83 | } 84 | 85 | return null; 86 | } 87 | 88 | /** 89 | * @inheritdoc 90 | * 91 | * Mount point is set lazily. 92 | */ 93 | public function setMountPoint($mountPoint) 94 | { 95 | if ($this->filesystem) { 96 | if ($this->filesystem instanceof MountPointAwareInterface) { 97 | $this->filesystem->setMountPoint($mountPoint); 98 | } 99 | } else { 100 | $this->mountPoint = $mountPoint; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Handler/FileInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface FileInterface extends HandlerInterface 15 | { 16 | /** 17 | * Read the file. 18 | * 19 | * @return string 20 | */ 21 | public function read(); 22 | 23 | /** 24 | * Read the file as a stream. 25 | * 26 | * @return StreamInterface 27 | */ 28 | public function readStream(); 29 | 30 | /** 31 | * Write the new file. 32 | * 33 | * @param string $content 34 | */ 35 | public function write($content); 36 | 37 | /** 38 | * Write the new file using a stream. 39 | * 40 | * @param StreamInterface|resource $resource 41 | */ 42 | public function writeStream($resource); 43 | 44 | /** 45 | * Update the file contents. 46 | * 47 | * @param string $content 48 | */ 49 | public function update($content); 50 | 51 | /** 52 | * Update the file contents with a stream. 53 | * 54 | * @param StreamInterface|resource $resource 55 | */ 56 | public function updateStream($resource); 57 | 58 | /** 59 | * Create the file or update if exists. 60 | * 61 | * @param string $content 62 | * 63 | * @return void 64 | */ 65 | public function put($content); 66 | 67 | /** 68 | * Create the file or update if exists using a stream. 69 | * 70 | * @param StreamInterface|resource $resource 71 | */ 72 | public function putStream($resource); 73 | 74 | /** 75 | * Rename the file. 76 | * 77 | * @param string $newPath 78 | */ 79 | public function rename($newPath); 80 | 81 | /** 82 | * Get the file's MIME Type. 83 | * 84 | * @return string 85 | */ 86 | public function getMimeType(); 87 | 88 | /** 89 | * Get the file size. 90 | * 91 | * @return int 92 | */ 93 | public function getSize(); 94 | 95 | /** 96 | * Get the file size in a human readable format. 97 | * 98 | * @param bool $si Return results according to IEC standards (ie. 4.60 KiB) or SI standards (ie. 4.7 kb) 99 | * 100 | * @return string 101 | */ 102 | public function getSizeFormatted($si = false); 103 | 104 | /** 105 | * Load the PHP file. 106 | * 107 | * @param bool $once Whether to include the file only once. 108 | * 109 | * @throws NotSupportedException If the filesystem does not support including PHP files. 110 | * @throws IncludeFileException On failure. 111 | * 112 | * @return mixed Returns the return from the file or true if $once is true and this is a subsequent call. 113 | */ 114 | public function includeFile($once = true); 115 | } 116 | -------------------------------------------------------------------------------- /src/Handler/Image/CoreType.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class CoreType extends Type implements TypeInterface 11 | { 12 | /** @var int */ 13 | private $id; 14 | /** @var string */ 15 | private $name; 16 | 17 | /** 18 | * Returns a list of all the core image types. 19 | * 20 | * @return TypeInterface[] 21 | */ 22 | public static function getTypes() 23 | { 24 | $types = []; 25 | foreach (static::getConstants() as $id => $name) { 26 | $types[] = new static($id, $name); 27 | } 28 | 29 | return $types; 30 | } 31 | 32 | /** 33 | * Constructor. 34 | * 35 | * @param int $id An IMAGETYPE_* constant 36 | * @param string $name String representation based on constant 37 | */ 38 | private function __construct($id, $name) 39 | { 40 | $this->id = (int) $id; 41 | $this->name = $name; 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function getId() 48 | { 49 | return $this->id; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getMimeType() 56 | { 57 | return image_type_to_mime_type($this->id); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function getExtension($includeDot = true) 64 | { 65 | return image_type_to_extension($this->id, $includeDot); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function toString() 72 | { 73 | return $this->name; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function __toString() 80 | { 81 | return $this->toString(); 82 | } 83 | 84 | /** 85 | * Returns a list of all the image type constants. 86 | * 87 | * @return array [int $id, string $name] 88 | */ 89 | private static function getConstants() 90 | { 91 | // Get list of all standard constants 92 | $constants = get_defined_constants(true); 93 | if (defined('HHVM_VERSION')) { 94 | $constants = $constants['Core']; 95 | } else { 96 | $constants = $constants['standard']; 97 | } 98 | 99 | // filter down to image type constants 100 | $types = []; 101 | foreach ($constants as $name => $value) { 102 | if ($value !== IMAGETYPE_COUNT && strpos($name, 'IMAGETYPE_') === 0) { 103 | $types[$name] = $value; 104 | } 105 | } 106 | 107 | // flip these and map them to a humanized string 108 | $types = array_map( 109 | function ($type) { 110 | return str_replace(['IMAGETYPE_', '_'], ['', ' '], $type); 111 | }, 112 | array_flip($types) 113 | ); 114 | 115 | ksort($types); 116 | 117 | return $types; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Iterator/FileContentFilterIteratorTest.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(new Local(__DIR__ . '/../')); 24 | } 25 | 26 | public function testAccept() 27 | { 28 | $inner = new \ArrayIterator(); 29 | $inner[] = new File($this->filesystem, 'base.css'); 30 | $iterator = new FileContentFilterIterator($inner, [], []); 31 | $this->assertIterator(['base.css'], $iterator); 32 | } 33 | 34 | public function testDirectory() 35 | { 36 | $inner = new \ArrayIterator(); 37 | $inner[] = new Directory($this->filesystem, 'fixtures'); 38 | $iterator = new FileContentFilterIterator($inner, ['fixtures'], []); 39 | $this->assertIterator([], $iterator); 40 | } 41 | 42 | public function testUnreadableFile() 43 | { 44 | $inner = new \ArrayIterator(); 45 | $mock = $this->getMockBuilder(File::class) 46 | ->setConstructorArgs([$this->filesystem, 'fixtures/base.css']) 47 | ->setMethods(['read']) 48 | ->getMock() 49 | ; 50 | $mock->expects($this->atLeastOnce()) 51 | ->method('read') 52 | ->will($this->throwException(new IOException('Fake it, until you make it!'))) 53 | ; 54 | $inner[] = $mock; 55 | $iterator = new FileContentFilterIterator($inner, ['fixtures/base.css'], []); 56 | $this->assertIterator([], $iterator); 57 | } 58 | 59 | /** 60 | * @dataProvider getTestFilterData 61 | */ 62 | public function testFilter(\Iterator $inner, array $matchPatterns, array $noMatchPatterns, array $resultArray) 63 | { 64 | $iterator = new FileContentFilterIterator($inner, $matchPatterns, $noMatchPatterns); 65 | $this->assertIterator($resultArray, $iterator); 66 | } 67 | 68 | public function getTestFilterData() 69 | { 70 | $filesystem = new Filesystem(new Local(__DIR__ . '/../')); 71 | $inner = new \ArrayIterator(); 72 | 73 | $inner[] = new File($filesystem, 'fixtures/base.css'); 74 | $inner[] = new File($filesystem, 'fixtures/css/style.css'); 75 | $inner[] = new File($filesystem, 'fixtures/js/script.js'); 76 | 77 | return [ 78 | [$inner, ['.'], [], ['fixtures/base.css', 'fixtures/css/style.css', 'fixtures/js/script.js']], 79 | [$inner, ['color'], [], ['fixtures/base.css', 'fixtures/css/style.css']], 80 | [$inner, ['color', 'koala'], ['width', 'shape'], ['fixtures/css/style.css']], 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Handler/DirectoryInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface DirectoryInterface extends HandlerInterface 15 | { 16 | /** 17 | * Returns whether this directory is the root directory. 18 | * 19 | * @return bool 20 | */ 21 | public function isRoot(); 22 | 23 | /** 24 | * Create the directory. 25 | * 26 | * @param array $config 27 | * 28 | * @throws DirectoryCreationException 29 | * @throws IOException 30 | */ 31 | public function create($config = []); 32 | 33 | /** 34 | * Mirrors the directory to another. 35 | * 36 | * Note: By default, this will delete files in target if they are not in source. 37 | * 38 | * @param string $targetDir The target directory 39 | * @param array $config Valid options are: 40 | * - delete = Whether to delete files that are not in the source directory (default: true) 41 | * - override = See {@see copyDir}'s $override parameter for details (default: null) 42 | */ 43 | public function mirror($targetDir, $config = []); 44 | 45 | /** 46 | * Get a handler for an entree. 47 | * 48 | * @param string $path The path to the file. 49 | * @param HandlerInterface $handler An optional existing handler to populate. 50 | * 51 | * @throws IOException 52 | * 53 | * @return HandlerInterface 54 | */ 55 | public function get($path, HandlerInterface $handler = null); 56 | 57 | /** 58 | * Get a file handler. 59 | * 60 | * @param string $path The path to the file. 61 | * @param FileInterface $handler An optional existing file handler to populate. 62 | * 63 | * @throws IOException 64 | * 65 | * @return FileInterface 66 | */ 67 | public function getFile($path, FileInterface $handler = null); 68 | 69 | /** 70 | * Get a directory handler. 71 | * 72 | * @param string $path The path to the directory. 73 | * 74 | * @throws IOException 75 | * 76 | * @return DirectoryInterface 77 | */ 78 | public function getDir($path); 79 | 80 | /** 81 | * Get an image handler. 82 | * 83 | * @param string $path The path to the image. 84 | * 85 | * @throws IOException 86 | * 87 | * @return ImageInterface 88 | */ 89 | public function getImage($path); 90 | 91 | /** 92 | * List the directory contents. 93 | * 94 | * @param bool $recursive 95 | * 96 | * @return HandlerInterface[] 97 | */ 98 | public function getContents($recursive = false); 99 | 100 | /** 101 | * Returns a finder instance set to this directory. 102 | * 103 | * @return Finder 104 | */ 105 | public function find(); 106 | } 107 | -------------------------------------------------------------------------------- /tests/Iterator/RecursiveDirectoryIteratorTest.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(new Local(__DIR__ . '/../')); 24 | } 25 | 26 | public function testIteration() 27 | { 28 | $it = new RecursiveDirectoryIterator($this->filesystem, 'fixtures'); 29 | $it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); 30 | 31 | $expected = [ 32 | 'fixtures/base.css', 33 | 'fixtures/css', 34 | 'fixtures/css/old', 35 | 'fixtures/css/old/old_style.css', 36 | 'fixtures/css/reset.css', 37 | 'fixtures/css/style.css', 38 | 'fixtures/images', 39 | 'fixtures/images/1-top-left.jpg', 40 | 'fixtures/images/2-top-right.jpg', 41 | 'fixtures/images/3-bottom-right.jpg', 42 | 'fixtures/images/4-bottom-left.jpg', 43 | 'fixtures/images/5-left-top.jpg', 44 | 'fixtures/images/6-right-top.jpg', 45 | 'fixtures/images/7-right-bottom.jpg', 46 | 'fixtures/images/8-left-bottom.jpg', 47 | 'fixtures/images/nut.svg', 48 | 'fixtures/js', 49 | 'fixtures/js/script.js', 50 | ]; 51 | $this->assertIterator($expected, $it); 52 | $this->assertIteratorInForeach($expected, $it); 53 | } 54 | 55 | public function testIterationForGlob() 56 | { 57 | $glob = '/fixtures/**/*.css'; 58 | $basePath = Glob::getBasePath($glob); 59 | $it = new RecursiveDirectoryIterator($this->filesystem, $basePath, RecursiveDirectoryIterator::KEY_FOR_GLOB); 60 | $it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); 61 | $it = new GlobFilterIterator($glob, $it, GlobFilterIterator::FILTER_KEY | GlobFilterIterator::KEY_AS_KEY); 62 | 63 | $expected = [ 64 | 'fixtures/base.css', 65 | 'fixtures/css/old/old_style.css', 66 | 'fixtures/css/reset.css', 67 | 'fixtures/css/style.css', 68 | ]; 69 | $this->assertIterator($expected, $it); 70 | $this->assertIteratorInForeach($expected, $it); 71 | } 72 | 73 | public function testSeek() 74 | { 75 | $it = new RecursiveDirectoryIterator($this->filesystem, 'fixtures'); 76 | 77 | $it->seek(1); 78 | $this->assertTrue($it->valid(), 'Current iteration is not valid'); 79 | $this->assertEquals('fixtures/css', $it->current()->getPath()); 80 | 81 | $it->seek(0); 82 | $this->assertTrue($it->valid(), 'Current iteration is not valid'); 83 | $this->assertEquals('fixtures/base.css', $it->current()->getPath()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Iterator/MapIterator.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class MapIterator implements OuterIterator 18 | { 19 | /** @var Iterator */ 20 | private $inner; 21 | 22 | /** @var mixed */ 23 | private $key = null; 24 | /** @var mixed */ 25 | private $current = null; 26 | 27 | /** 28 | * Constructor. 29 | * 30 | * @param Traversable|array $iterable 31 | */ 32 | public function __construct($iterable) 33 | { 34 | if ($iterable instanceof Traversable) { 35 | $this->inner = new IteratorIterator($iterable); 36 | } elseif (is_array($iterable)) { 37 | $this->inner = new ArrayIterator($iterable); 38 | } else { 39 | throw new \InvalidArgumentException('MapIterator must be given an iterable object.'); 40 | } 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function getInnerIterator() 47 | { 48 | return $this->inner; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function valid() 55 | { 56 | return $this->inner->valid(); 57 | } 58 | 59 | /** 60 | * {@inheritdoc} 61 | */ 62 | public function key() 63 | { 64 | return $this->key; 65 | } 66 | 67 | /** 68 | * {@inheritdoc} 69 | */ 70 | public function current() 71 | { 72 | return $this->current; 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function next() 79 | { 80 | $this->inner->next(); 81 | $this->applyMapping(); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function rewind() 88 | { 89 | $this->inner->rewind(); 90 | $this->applyMapping(); 91 | } 92 | 93 | /** 94 | * Return the iterable as an array (with the mapping applied). 95 | * 96 | * @return array 97 | */ 98 | public function toArray() 99 | { 100 | return iterator_to_array($this); 101 | } 102 | 103 | /** 104 | * Map the current key and/or value to something else. 105 | * 106 | * @param mixed $value The value. 107 | * @param mixed $key Key is passed by reference so it can be changed as well. 108 | * 109 | * @return mixed The new value. 110 | */ 111 | abstract protected function map($value, &$key); 112 | 113 | /** 114 | * Apply mapping functions to key/value if current entry is valid. 115 | */ 116 | private function applyMapping() 117 | { 118 | if (!$this->valid()) { 119 | return; 120 | } 121 | 122 | // Store key as local variable since it is passed by reference. 123 | $key = $this->inner->key(); 124 | $this->current = $this->map($this->inner->current(), $key); 125 | $this->key = $key; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Plugin/PluggableTrait.php: -------------------------------------------------------------------------------- 1 | plugins[$plugin->getMethod()] = $plugin; 25 | } 26 | 27 | /** 28 | * Register a list of plugins. 29 | * 30 | * @param PluginInterface[] $plugins 31 | * 32 | * @throws InvalidArgumentException 33 | */ 34 | public function addPlugins(array $plugins) 35 | { 36 | foreach ($plugins as $plugin) { 37 | if (!$plugin instanceof PluginInterface) { 38 | throw new InvalidArgumentException('Plugin must be instance of Bolt\Filesystem\PluginInterface'); 39 | } 40 | $this->addPlugin($plugin); 41 | } 42 | } 43 | 44 | /** 45 | * Find a specific plugin. 46 | * 47 | * @param string $method 48 | * 49 | * @throws LogicException 50 | * 51 | * @return PluginInterface $plugin 52 | */ 53 | protected function findPlugin($method) 54 | { 55 | if (!isset($this->plugins[$method])) { 56 | throw new PluginNotFoundException('Plugin not found for method: ' . $method); 57 | } 58 | 59 | if (!method_exists($this->plugins[$method], 'handle')) { 60 | throw new LogicException(get_class($this->plugins[$method]) . ' does not have a handle method.'); 61 | } 62 | 63 | return $this->plugins[$method]; 64 | } 65 | 66 | /** 67 | * Invoke a plugin by method name. 68 | * 69 | * @param string $method 70 | * @param array $arguments 71 | * @param FilesystemInterface $filesystem 72 | * 73 | * @return mixed 74 | */ 75 | protected function invokePlugin($method, array $arguments, FilesystemInterface $filesystem) 76 | { 77 | $plugin = $this->findPlugin($method); 78 | $plugin->setFilesystem($filesystem); 79 | $callback = [$plugin, 'handle']; 80 | 81 | return call_user_func_array($callback, $arguments); 82 | } 83 | 84 | /** 85 | * Plugins pass-through. 86 | * 87 | * @param string $method 88 | * @param array $arguments 89 | * 90 | * @throws BadMethodCallException 91 | * 92 | * @return mixed 93 | */ 94 | public function __call($method, array $arguments) 95 | { 96 | try { 97 | return $this->invokePlugin($method, $arguments, $this); 98 | } catch (PluginNotFoundException $e) { 99 | throw new BadMethodCallException( 100 | 'Call to undefined method ' 101 | . get_class($this) 102 | . '::' . $method 103 | ); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/Iterator/PathFilterIteratorTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class PathFilterIteratorTest extends IteratorTestCase 18 | { 19 | /** @var FilesystemInterface */ 20 | protected $filesystem; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function setUp() 26 | { 27 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../')); 28 | } 29 | 30 | /** 31 | * @dataProvider getTestFilterData 32 | */ 33 | public function testFilter(\Iterator $inner, array $matchPatterns, array $noMatchPatterns, array $resultArray) 34 | { 35 | $iterator = new PathFilterIterator($inner, $matchPatterns, $noMatchPatterns); 36 | $this->assertIterator($resultArray, $iterator); 37 | } 38 | 39 | public function getTestFilterData() 40 | { 41 | $inner = new MockFileListIterator(); 42 | 43 | $inner[] = new File($this->filesystem, 'A/B/C/abc.dat'); 44 | $inner[] = new File($this->filesystem, 'A/B/ab.dat'); 45 | $inner[] = new File($this->filesystem, 'A/a.dat'); 46 | $inner[] = new File($this->filesystem, 'copy/A/B/C/abc.dat.copy'); 47 | $inner[] = new File($this->filesystem, 'copy/A/B/ab.dat.copy'); 48 | $inner[] = new File($this->filesystem, 'copy/A/a.dat.copy'); 49 | 50 | return [ 51 | [$inner, ['/^A/'], [], ['A/B/C/abc.dat', 'A/B/ab.dat', 'A/a.dat']], 52 | [$inner, ['/^A\/B/'], [], ['A/B/C/abc.dat', 'A/B/ab.dat']], 53 | [$inner, ['/^A\/B\/C/'], [], ['A/B/C/abc.dat']], 54 | [$inner, ['/A\/B\/C/'], [], ['A/B/C/abc.dat', 'copy/A/B/C/abc.dat.copy']], 55 | 56 | [$inner, ['A'], [], ['A/B/C/abc.dat', 'A/B/ab.dat', 'A/a.dat', 'copy/A/B/C/abc.dat.copy', 'copy/A/B/ab.dat.copy', 'copy/A/a.dat.copy']], 57 | [$inner, ['A/B'], [], ['A/B/C/abc.dat', 'A/B/ab.dat', 'copy/A/B/C/abc.dat.copy', 'copy/A/B/ab.dat.copy']], 58 | [$inner, ['A/B'], [], ['A/B/C/abc.dat', 'A/B/ab.dat', 'copy/A/B/C/abc.dat.copy', 'copy/A/B/ab.dat.copy']], 59 | [$inner, ['A/B/C'], [], ['A/B/C/abc.dat', 'copy/A/B/C/abc.dat.copy']], 60 | 61 | [$inner, ['copy/A'], [], ['copy/A/B/C/abc.dat.copy', 'copy/A/B/ab.dat.copy', 'copy/A/a.dat.copy']], 62 | [$inner, ['copy/A/B'], [], ['copy/A/B/C/abc.dat.copy', 'copy/A/B/ab.dat.copy']], 63 | [$inner, ['copy/A/B/C'], [], ['copy/A/B/C/abc.dat.copy']], 64 | 65 | [$inner, ['A'], ['/copy/'], ['A/B/C/abc.dat', 'A/B/ab.dat', 'A/a.dat']], 66 | [$inner, ['A/B'], ['/copy/'], ['A/B/C/abc.dat', 'A/B/ab.dat']], 67 | [$inner, ['A/B'], ['/copy/'], ['A/B/C/abc.dat', 'A/B/ab.dat']], 68 | [$inner, ['A/B/C'], ['/copy/'], ['A/B/C/abc.dat']], 69 | ]; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/Adapter/LocalTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class LocalTest extends FilesystemTestCase 18 | { 19 | /** @var FilesystemInterface */ 20 | protected $filesystem; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function setUp() 26 | { 27 | parent::setUp(); 28 | $this->filesystem = new Filesystem(new Local($this->rootDir . '/tests')); 29 | } 30 | 31 | /** 32 | * @expectedException \Bolt\Filesystem\Exception\DirectoryCreationException 33 | * @expectedExceptionMessage Failed to create directory 34 | */ 35 | public function testConstruct() 36 | { 37 | if (posix_getuid() === 0) { 38 | $this->fail('Do not run as root user'); 39 | } 40 | 41 | $local = new Local($this->tempDir); 42 | $this->assertInstanceOf(Local::class, $local); 43 | 44 | new Local('/bad'); 45 | } 46 | 47 | public function testUpdate() 48 | { 49 | $this->filesystem->get('fixtures/base.css')->copy('temp/koala.css'); 50 | $local = new Local($this->tempDir); 51 | $config = new Config(); 52 | 53 | $update = $local->update('koala.css', '.drop-bear {}', $config); 54 | $this->assertSame('koala.css', $update['path']); 55 | $this->assertSame('.drop-bear {}', $update['contents']); 56 | $this->assertSame('text/css', $update['mimetype']); 57 | 58 | $update = $local->update('koala.css.typo', '.drop-bear {}', $config); 59 | $this->assertFalse($update); 60 | } 61 | 62 | /** 63 | * @expectedException \Bolt\Filesystem\Exception\FileNotFoundException 64 | */ 65 | public function testDelete() 66 | { 67 | $this->filesystem->get('fixtures/base.css')->copy('temp/koala.css'); 68 | $local = new Local($this->tempDir); 69 | $delete = $local->delete('koala.css'); 70 | $this->assertTrue($delete); 71 | 72 | $local->delete('koala.css.typo'); 73 | } 74 | 75 | public function testCreateDir() 76 | { 77 | $local = new Local($this->tempDir); 78 | $config = new Config(); 79 | 80 | $create = $local->createDir('horse-with-no-name', $config); 81 | $this->assertSame('horse-with-no-name', $create['path']); 82 | $this->assertSame('dir', $create['type']); 83 | 84 | $this->filesystem->get('fixtures/base.css')->copy('temp/horse-with-no-name/koala.css'); 85 | $create = $local->createDir('horse-with-no-name/koala.css', $config); 86 | $this->assertFalse($create); 87 | } 88 | 89 | public function testDeleteDir() 90 | { 91 | $local = new Local($this->tempDir); 92 | $config = new Config(); 93 | 94 | $local->createDir('horse-with-no-name', $config); 95 | $delete = $local->deleteDir('horse-with-no-name'); 96 | 97 | $this->assertTrue($delete); 98 | $this->assertFalse($local->has('horse-with-no-name')); 99 | 100 | $delete = $local->deleteDir('horse-with-no-name'); 101 | $this->assertFalse($delete); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Handler/Image/Type.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Type 15 | { 16 | /** @var TypeInterface[] */ 17 | private static $types = []; 18 | /** @var bool */ 19 | private static $initialized = false; 20 | 21 | /** 22 | * Register an Image Type. 23 | * 24 | * @param TypeInterface $type 25 | */ 26 | public static function register(TypeInterface $type) 27 | { 28 | static::$types[$type->getId()] = $type; 29 | } 30 | 31 | /** 32 | * Returns a Type for the ID. 33 | * 34 | * @param int $id An IMAGETYPE_* constant 35 | * 36 | * @throws InvalidArgumentException If the ID isn't a valid IMAGETYPE_* constant 37 | * 38 | * @return TypeInterface 39 | */ 40 | public static function getById($id) 41 | { 42 | $id = (int) $id; 43 | $types = static::getTypes(); 44 | 45 | if (!isset($types[$id])) { 46 | throw new InvalidArgumentException('Given type is not an IMAGETYPE_* constant'); 47 | } 48 | 49 | return $types[$id]; 50 | } 51 | 52 | /** 53 | * Returns a list of all the image types. 54 | * 55 | * @return TypeInterface[] 56 | */ 57 | public static function getTypes() 58 | { 59 | static::initialize(); 60 | 61 | return static::$types; 62 | } 63 | 64 | /** 65 | * Returns a list of all the MIME Types for images. 66 | * 67 | * @return string[] 68 | */ 69 | public static function getMimeTypes() 70 | { 71 | return array_map( 72 | function (TypeInterface $type) { 73 | return $type->getMimeType(); 74 | }, 75 | static::getTypes() 76 | ); 77 | } 78 | 79 | /** 80 | * Returns a list of all the file extensions for images. 81 | * 82 | * @param bool $includeDot Whether to prepend a dot to the extension or not 83 | * 84 | * @return string[] 85 | */ 86 | public static function getExtensions($includeDot = false) 87 | { 88 | $extensions = array_filter( 89 | array_map( 90 | function (TypeInterface $type) use ($includeDot) { 91 | return $type->getExtension($includeDot); 92 | }, 93 | static::getTypes() 94 | ) 95 | ); 96 | $extensions[] = ($includeDot ? '.' : '') . 'jpg'; 97 | 98 | return $extensions; 99 | } 100 | 101 | /** 102 | * Shortcut for unknown image type. 103 | * 104 | * @return TypeInterface 105 | */ 106 | public static function unknown() 107 | { 108 | return static::getById(IMAGETYPE_UNKNOWN); 109 | } 110 | 111 | /** 112 | * Register default types. 113 | */ 114 | private static function initialize() 115 | { 116 | if (static::$initialized) { 117 | return; 118 | } 119 | static::$initialized = true; 120 | 121 | foreach (CoreType::getTypes() as $type) { 122 | static::register($type); 123 | } 124 | 125 | static::register(new SvgType()); 126 | } 127 | 128 | /** 129 | * Constructor. 130 | */ 131 | private function __construct() 132 | { 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Iterator/DateRangeFilterIteratorTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class DateRangeFilterIteratorTest extends IteratorTestCase 19 | { 20 | /** @var FilesystemInterface */ 21 | protected $filesystem; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | protected function setUp() 27 | { 28 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../')); 29 | } 30 | 31 | /** 32 | * @dataProvider getAcceptData 33 | */ 34 | public function testAccept($size, $expected) 35 | { 36 | $it = new RecursiveDirectoryIterator($this->filesystem, 'fixtures'); 37 | $it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); 38 | 39 | touch(dirname(__DIR__) . '/fixtures/css/old', strtotime('2012-06-14')); 40 | touch(dirname(__DIR__) . '/fixtures/css/old/old_style.css', strtotime('2012-06-14')); 41 | 42 | $iterator = new DateRangeFilterIterator($it, $size); 43 | 44 | $this->assertIterator($expected, $iterator); 45 | } 46 | 47 | public function getAcceptData() 48 | { 49 | $since20YearsAgo = [ 50 | 'fixtures/base.css', 51 | 'fixtures/css', 52 | 'fixtures/css/old', 53 | 'fixtures/css/old/old_style.css', 54 | 'fixtures/css/reset.css', 55 | 'fixtures/css/style.css', 56 | 'fixtures/images', 57 | 'fixtures/images/1-top-left.jpg', 58 | 'fixtures/images/2-top-right.jpg', 59 | 'fixtures/images/3-bottom-right.jpg', 60 | 'fixtures/images/4-bottom-left.jpg', 61 | 'fixtures/images/5-left-top.jpg', 62 | 'fixtures/images/6-right-top.jpg', 63 | 'fixtures/images/7-right-bottom.jpg', 64 | 'fixtures/images/8-left-bottom.jpg', 65 | 'fixtures/images/nut.svg', 66 | 'fixtures/js', 67 | 'fixtures/js/script.js', 68 | ]; 69 | 70 | $since2MonthsAgo = [ 71 | 'fixtures/base.css', 72 | 'fixtures/css', 73 | 'fixtures/css/reset.css', 74 | 'fixtures/css/style.css', 75 | 'fixtures/images', 76 | 'fixtures/images/1-top-left.jpg', 77 | 'fixtures/images/2-top-right.jpg', 78 | 'fixtures/images/3-bottom-right.jpg', 79 | 'fixtures/images/4-bottom-left.jpg', 80 | 'fixtures/images/5-left-top.jpg', 81 | 'fixtures/images/6-right-top.jpg', 82 | 'fixtures/images/7-right-bottom.jpg', 83 | 'fixtures/images/8-left-bottom.jpg', 84 | 'fixtures/images/nut.svg', 85 | 'fixtures/js', 86 | 'fixtures/js/script.js', 87 | ]; 88 | 89 | $untilLastMonth = [ 90 | 'fixtures/css/old', 91 | 'fixtures/css/old/old_style.css', 92 | ]; 93 | 94 | return [ 95 | [[new DateComparator('since 20 years ago')], $since20YearsAgo], 96 | [[new DateComparator('since 2 months ago')], $since2MonthsAgo], 97 | [[new DateComparator('until last month')], $untilLastMonth], 98 | ]; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Adapter/Cached.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Cached extends CachedAdapter implements Capability\ImageInfo, Capability\IncludeFile 19 | { 20 | /** @var CacheInterface */ 21 | protected $cache; 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function __construct(AdapterInterface $adapter, CacheInterface $cache) 27 | { 28 | parent::__construct($adapter, $cache); 29 | $this->cache = $cache; 30 | } 31 | 32 | /** 33 | * Flush the cache. 34 | */ 35 | public function flush() 36 | { 37 | $this->cache->flush(); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function read($path) 44 | { 45 | $result = $this->cache->read($path); 46 | 47 | if ($result !== false && isset($result['contents']) && $result['contents'] !== false) { 48 | return $result; 49 | } 50 | 51 | $result = $this->getAdapter()->read($path); 52 | 53 | if ($result) { 54 | $object = $result + compact('path'); 55 | $this->cache->updateObject($path, $object, true); 56 | } 57 | 58 | return $result; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getImageInfo($path) 65 | { 66 | // If cache doesn't support image info, just pass through to adapter. 67 | if (!$this->cache instanceof Capability\ImageInfo) { 68 | return $this->doGetImageInfo($path); 69 | } 70 | 71 | // Get from cache. 72 | $info = $this->cache->getImageInfo($path); 73 | if ($info !== false) { 74 | return is_array($info) ? Image\Info::createFromJson($info) : $info; 75 | } 76 | 77 | // Else from adapter. 78 | $info = $this->doGetImageInfo($path); 79 | 80 | // Save info from adapter. 81 | $object = [ 82 | 'path' => $path, 83 | 'image_info' => $info, 84 | ]; 85 | $this->cache->updateObject($path, $object, true); 86 | 87 | return $info; 88 | } 89 | 90 | /** 91 | * Get image info from adapter. 92 | * 93 | * @param string $path 94 | * 95 | * @return Image\Info 96 | */ 97 | private function doGetImageInfo($path) 98 | { 99 | // Get info from adapter if it's capable. 100 | $adapter = $this->getAdapter(); 101 | if ($adapter instanceof Capability\ImageInfo) { 102 | return $adapter->getImageInfo($path); 103 | } 104 | 105 | // Else fallback to reading image contents and creating info from string. 106 | $result = $this->read($path); 107 | if ($result === false || !isset($result['contents'])) { 108 | throw new IOException('Failed to read file', $path); 109 | } 110 | 111 | return Image\Info::createFromString($result['contents'], $path); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function includeFile($path, $once = true) 118 | { 119 | $adapter = $this->getAdapter(); 120 | if (!$adapter instanceof Capability\IncludeFile) { 121 | throw new NotSupportedException('Filesystem does not support including PHP files.'); 122 | } 123 | 124 | return $adapter->includeFile($path, $once); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Handler/YamlFile.php: -------------------------------------------------------------------------------- 1 | 15 | * @author Carson Full 16 | */ 17 | class YamlFile extends File implements ParsableInterface 18 | { 19 | /** @var bool Whether symfony/yaml is v3.1+ */ 20 | private static $useFlags; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | public function parse($options = []) 26 | { 27 | $options += [ 28 | 'exceptionsOnInvalidType' => false, 29 | 'objectSupport' => false, 30 | 'objectForMap' => false, 31 | ]; 32 | 33 | $contents = $this->read(); 34 | 35 | static::checkYamlVersion(); 36 | 37 | try { 38 | if (static::$useFlags) { 39 | $flags = $this->optionsToFlags($options); 40 | 41 | return Yaml::parse($contents, $flags); 42 | } else { 43 | return Yaml::parse( 44 | $contents, 45 | $options['exceptionsOnInvalidType'], 46 | $options['objectSupport'], 47 | $options['objectForMap'] 48 | ); 49 | } 50 | } catch (Symfony\ParseException $e) { 51 | throw ParseException::castFromYaml($e); 52 | } 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function dump($contents, $options = []) 59 | { 60 | $options += [ 61 | 'inline' => 2, 62 | 'indent' => 4, 63 | 'exceptionsOnInvalidType' => false, 64 | 'objectSupport' => false, 65 | ]; 66 | 67 | static::checkYamlVersion(); 68 | 69 | try { 70 | if (static::$useFlags) { 71 | $flags = $this->optionsToFlags($options); 72 | $contents = Yaml::dump($contents, $options['inline'], $options['indent'], $flags); 73 | } else { 74 | $contents = Yaml::dump( 75 | $contents, 76 | $options['inline'], 77 | $options['indent'], 78 | $options['exceptionsOnInvalidType'], 79 | $options['objectSupport'] 80 | ); 81 | } 82 | } catch (Symfony\DumpException $e) { 83 | throw new DumpException($e->getMessage(), $e->getCode(), $e); 84 | } 85 | $this->put($contents); 86 | } 87 | 88 | /** 89 | * @deprecated Remove when symfony/yaml 3.1+ is required 90 | * 91 | * @param array $options 92 | * 93 | * @return int 94 | */ 95 | private function optionsToFlags(array $options) 96 | { 97 | $flagParams = [ 98 | 'exceptionsOnInvalidType' => Yaml::PARSE_EXCEPTION_ON_INVALID_TYPE, 99 | 'objectSupport' => Yaml::PARSE_OBJECT, 100 | 'objectForMap' => Yaml::PARSE_OBJECT_FOR_MAP, 101 | ]; 102 | 103 | $flags = 0; 104 | foreach ($flagParams as $optionName => $bit) { 105 | if (isset($options[$optionName]) && $options[$optionName]) { 106 | $flags |= $bit; 107 | } 108 | } 109 | 110 | return $flags; 111 | } 112 | 113 | /** 114 | * @deprecated Remove when symfony/yaml 3.1+ is required 115 | */ 116 | private static function checkYamlVersion() 117 | { 118 | if (static::$useFlags === null) { 119 | $ref = new ReflectionMethod(Yaml::class, 'parse'); 120 | static::$useFlags = $ref->getNumberOfParameters() === 2; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /tests/Iterator/ExcludeDirectoryFilterIteratorTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class ExcludeDirectoryFilterIteratorTest extends IteratorTestCase 18 | { 19 | /** @var FilesystemInterface */ 20 | protected $filesystem; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function setUp() 26 | { 27 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../')); 28 | } 29 | 30 | /** 31 | * @dataProvider getAcceptData 32 | */ 33 | public function testAccept($directories, $expected) 34 | { 35 | $it = new RecursiveDirectoryIterator($this->filesystem, 'fixtures'); 36 | $it = new ExcludeDirectoryFilterIterator($it, $directories); 37 | $it = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST); 38 | 39 | $this->assertIterator($expected, $it); 40 | } 41 | 42 | public function getAcceptData() 43 | { 44 | return [ 45 | 'exclude directory name' => [ 46 | ['js'], 47 | [ 48 | 'fixtures/base.css', 49 | 'fixtures/css', 50 | 'fixtures/css/old', 51 | 'fixtures/css/old/old_style.css', 52 | 'fixtures/css/reset.css', 53 | 'fixtures/css/style.css', 54 | 'fixtures/images', 55 | 'fixtures/images/1-top-left.jpg', 56 | 'fixtures/images/2-top-right.jpg', 57 | 'fixtures/images/3-bottom-right.jpg', 58 | 'fixtures/images/4-bottom-left.jpg', 59 | 'fixtures/images/5-left-top.jpg', 60 | 'fixtures/images/6-right-top.jpg', 61 | 'fixtures/images/7-right-bottom.jpg', 62 | 'fixtures/images/8-left-bottom.jpg', 63 | 'fixtures/images/nut.svg', 64 | ] 65 | ], 66 | 'partial dir names do not count' => [ 67 | ['j'], 68 | [ 69 | 'fixtures/base.css', 70 | 'fixtures/css', 71 | 'fixtures/css/old', 72 | 'fixtures/css/old/old_style.css', 73 | 'fixtures/css/reset.css', 74 | 'fixtures/css/style.css', 75 | 'fixtures/images', 76 | 'fixtures/images/1-top-left.jpg', 77 | 'fixtures/images/2-top-right.jpg', 78 | 'fixtures/images/3-bottom-right.jpg', 79 | 'fixtures/images/4-bottom-left.jpg', 80 | 'fixtures/images/5-left-top.jpg', 81 | 'fixtures/images/6-right-top.jpg', 82 | 'fixtures/images/7-right-bottom.jpg', 83 | 'fixtures/images/8-left-bottom.jpg', 84 | 'fixtures/images/nut.svg', 85 | 'fixtures/js', 86 | 'fixtures/js/script.js', 87 | ] 88 | ], 89 | 'pattern' => [ 90 | ['css/old'], 91 | [ 92 | 'fixtures/base.css', 93 | 'fixtures/css', 94 | 'fixtures/css/reset.css', 95 | 'fixtures/css/style.css', 96 | 'fixtures/images', 97 | 'fixtures/images/1-top-left.jpg', 98 | 'fixtures/images/2-top-right.jpg', 99 | 'fixtures/images/3-bottom-right.jpg', 100 | 'fixtures/images/4-bottom-left.jpg', 101 | 'fixtures/images/5-left-top.jpg', 102 | 'fixtures/images/6-right-top.jpg', 103 | 'fixtures/images/7-right-bottom.jpg', 104 | 'fixtures/images/8-left-bottom.jpg', 105 | 'fixtures/images/nut.svg', 106 | 'fixtures/js', 107 | 'fixtures/js/script.js', 108 | ] 109 | ] 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Handler/File.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class File extends BaseHandler implements FileInterface 11 | { 12 | /** 13 | * {@inheritdoc} 14 | */ 15 | public function read() 16 | { 17 | return $this->filesystem->read($this->path); 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function readStream() 24 | { 25 | return $this->filesystem->readStream($this->path); 26 | } 27 | 28 | /** 29 | * {@inheritdoc} 30 | */ 31 | public function includeFile($once = true) 32 | { 33 | return $this->filesystem->includeFile($this->path, $once); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function write($content) 40 | { 41 | $this->filesystem->write($this->path, $content); 42 | } 43 | 44 | /** 45 | * {@inheritdoc} 46 | */ 47 | public function writeStream($resource) 48 | { 49 | $this->filesystem->writeStream($this->path, $resource); 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function update($content) 56 | { 57 | $this->filesystem->update($this->path, $content); 58 | } 59 | 60 | /** 61 | * {@inheritdoc} 62 | */ 63 | public function updateStream($resource) 64 | { 65 | $this->filesystem->updateStream($this->path, $resource); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function put($content) 72 | { 73 | $this->filesystem->put($this->path, $content); 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function putStream($resource) 80 | { 81 | $this->filesystem->putStream($this->path, $resource); 82 | } 83 | 84 | /** 85 | * {@inheritdoc} 86 | */ 87 | public function rename($newPath) 88 | { 89 | $this->filesystem->rename($this->path, $newPath); 90 | $this->path = $newPath; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function copy($target, $override = null) 97 | { 98 | $this->filesystem->copy($this->path, $target, $override); 99 | 100 | return new static($this->filesystem, $target); 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function delete() 107 | { 108 | $this->filesystem->delete($this->path); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function getMimeType() 115 | { 116 | return $this->filesystem->getMimeType($this->path); 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | public function getSize() 123 | { 124 | return $this->filesystem->getSize($this->path); 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function getSizeFormatted($si = false) 131 | { 132 | $size = $this->getSize(); 133 | 134 | if ($si) { 135 | return $this->getSizeFormattedSi($size); 136 | } else { 137 | return $this->getSizeFormattedExact($size); 138 | } 139 | } 140 | 141 | /** 142 | * Format a filesize according to IEC standard. For example: '4734 bytes' -> '4.62 KiB' 143 | * 144 | * @param int $size 145 | * 146 | * @return string 147 | */ 148 | private function getSizeFormattedExact($size) 149 | { 150 | if ($size > 1024 * 1024) { 151 | return sprintf('%0.2f MiB', ($size / 1024 / 1024)); 152 | } elseif ($size > 1024) { 153 | return sprintf('%0.2f KiB', ($size / 1024)); 154 | } else { 155 | return $size . ' B'; 156 | } 157 | } 158 | 159 | /** 160 | * Format a filesize as 'end user friendly', so this should be seen as something that'd 161 | * be used in a quick glance. For example: '4734 bytes' -> '4.7 kB' 162 | * 163 | * @param int $size 164 | * 165 | * @return string 166 | */ 167 | private function getSizeFormattedSi($size) 168 | { 169 | if ($size > 1000 * 1000) { 170 | return sprintf('%0.1f MB', ($size / 1000 / 1000)); 171 | } elseif ($size > 1000) { 172 | return sprintf('%0.1f KB', ($size / 1000)); 173 | } else { 174 | return $size . ' B'; 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/FilesystemWrapperTrait.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | trait FilesystemWrapperTrait // implements FilesystemInterface 14 | { 15 | /** 16 | * @return FilesystemInterface 17 | */ 18 | abstract protected function wrapped(); 19 | 20 | public function has($path) 21 | { 22 | return $this->wrapped()->has($path); 23 | } 24 | 25 | public function read($path) 26 | { 27 | return $this->wrapped()->read($path); 28 | } 29 | 30 | public function readStream($path) 31 | { 32 | return $this->wrapped()->readStream($path); 33 | } 34 | 35 | public function write($path, $contents, $config = []) 36 | { 37 | $this->wrapped()->write($path, $contents, $config); 38 | } 39 | 40 | public function writeStream($path, $resource, $config = []) 41 | { 42 | $this->wrapped()->writeStream($path, $resource, $config); 43 | } 44 | 45 | public function update($path, $contents, $config = []) 46 | { 47 | $this->wrapped()->update($path, $contents, $config); 48 | } 49 | 50 | public function updateStream($path, $resource, $config = []) 51 | { 52 | $this->wrapped()->updateStream($path, $resource, $config); 53 | } 54 | 55 | public function put($path, $contents, $config = []) 56 | { 57 | $this->wrapped()->put($path, $contents, $config); 58 | } 59 | 60 | public function putStream($path, $resource, $config = []) 61 | { 62 | $this->wrapped()->putStream($path, $resource, $config); 63 | } 64 | 65 | public function readAndDelete($path) 66 | { 67 | return $this->wrapped()->readAndDelete($path); 68 | } 69 | 70 | public function rename($path, $newPath) 71 | { 72 | $this->wrapped()->rename($path, $newPath); 73 | } 74 | 75 | public function copy($origin, $target, $override = null) 76 | { 77 | $this->wrapped()->copy($origin, $target, $override); 78 | } 79 | 80 | public function delete($path) 81 | { 82 | $this->wrapped()->delete($path); 83 | } 84 | 85 | public function deleteDir($dirname) 86 | { 87 | $this->wrapped()->deleteDir($dirname); 88 | } 89 | 90 | public function createDir($dirname, $config = []) 91 | { 92 | $this->wrapped()->createDir($dirname, $config); 93 | } 94 | 95 | public function copyDir($originDir, $targetDir, $override = null) 96 | { 97 | $this->wrapped()->copyDir($originDir, $targetDir, $override); 98 | } 99 | 100 | public function mirror($originDir, $targetDir, $config = []) 101 | { 102 | $this->wrapped()->mirror($originDir, $targetDir, $config); 103 | } 104 | 105 | public function get($path, HandlerInterface $handler = null) 106 | { 107 | return $this->wrapped()->get($path, $handler); 108 | } 109 | 110 | public function getFile($path, FileInterface $handler = null) 111 | { 112 | return $this->wrapped()->getFile($path, $handler); 113 | } 114 | 115 | public function getDir($path) 116 | { 117 | return $this->wrapped()->getDir($path); 118 | } 119 | 120 | public function getImage($path) 121 | { 122 | return $this->wrapped()->getImage($path); 123 | } 124 | 125 | public function getType($path) 126 | { 127 | return $this->wrapped()->getType($path); 128 | } 129 | 130 | public function getSize($path) 131 | { 132 | return $this->wrapped()->getSize($path); 133 | } 134 | 135 | public function getTimestamp($path) 136 | { 137 | return $this->wrapped()->getTimestamp($path); 138 | } 139 | 140 | public function getCarbon($path) 141 | { 142 | return $this->wrapped()->getCarbon($path); 143 | } 144 | 145 | public function getMimeType($path) 146 | { 147 | return $this->wrapped()->getMimeType($path); 148 | } 149 | 150 | public function getVisibility($path) 151 | { 152 | return $this->wrapped()->getVisibility($path); 153 | } 154 | 155 | public function setVisibility($path, $visibility) 156 | { 157 | $this->wrapped()->setVisibility($path, $visibility); 158 | } 159 | 160 | public function listContents($directory = '', $recursive = false) 161 | { 162 | return $this->wrapped()->listContents($directory, $recursive); 163 | } 164 | 165 | public function find() 166 | { 167 | return $this->wrapped()->find(); 168 | } 169 | 170 | public function getImageInfo($path) 171 | { 172 | return $this->wrapped()->getImageInfo($path); 173 | } 174 | 175 | public function includeFile($path, $once = true) 176 | { 177 | return $this->wrapped()->includeFile($path, $once); 178 | } 179 | 180 | public function addPlugin(PluginInterface $plugin) 181 | { 182 | $this->wrapped()->addPlugin($plugin); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/Handler/HandlerInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface HandlerInterface extends MountPointAwareInterface 15 | { 16 | /** 17 | * Set the Filesystem object. 18 | * 19 | * WARNING: Do not call this unless you know what you are doing. 20 | * 21 | * @param FilesystemInterface $filesystem 22 | * 23 | * @internal 24 | */ 25 | public function setFilesystem(FilesystemInterface $filesystem); 26 | 27 | /** 28 | * Returns the Filesystem object. 29 | * 30 | * @return FilesystemInterface 31 | */ 32 | public function getFilesystem(); 33 | 34 | /** 35 | * Set the entree path. 36 | * 37 | * WARNING: Do not call this unless you know what you are doing. 38 | * 39 | * @param string $path 40 | * 41 | * @internal 42 | */ 43 | public function setPath($path); 44 | 45 | /** 46 | * Returns the entree path. 47 | * 48 | * @return string path 49 | */ 50 | public function getPath(); 51 | 52 | /** 53 | * Returns the entree path with the mount point prefixed (if set). 54 | * 55 | * @return string 56 | */ 57 | public function getFullPath(); 58 | 59 | /** 60 | * Returns the directory for this entree. 61 | * 62 | * Note: If this entree is the root directory, a different 63 | * instance of the same directory is returned. 64 | * This can also be checked with {@see DirectoryInterface::isRoot} 65 | * 66 | * @return DirectoryInterface 67 | */ 68 | public function getParent(); 69 | 70 | /** 71 | * Returns whether the entree exists. 72 | * 73 | * @return bool 74 | */ 75 | public function exists(); 76 | 77 | /** 78 | * Delete the entree. 79 | */ 80 | public function delete(); 81 | 82 | /** 83 | * Copy the file/directory. 84 | * 85 | * By default, if the target already exists, it is only overridden if the source is newer. 86 | * 87 | * @param string $target Path to the target file. 88 | * @param bool|null $override Whether to override an existing file. 89 | * true = always override the target. 90 | * false = never override the target. 91 | * null = only override the target if the source is newer. 92 | */ 93 | public function copy($target, $override = null); 94 | 95 | /** 96 | * Returns whether the entree is a directory. 97 | * 98 | * @return bool 99 | */ 100 | public function isDir(); 101 | 102 | /** 103 | * Returns whether the entree is a file. 104 | * 105 | * @return bool 106 | */ 107 | public function isFile(); 108 | 109 | /** 110 | * Returns whether the entree is a image. 111 | * 112 | * @return bool 113 | */ 114 | public function isImage(); 115 | 116 | /** 117 | * Returns whether the entree is a document. 118 | * 119 | * @return bool 120 | */ 121 | public function isDocument(); 122 | 123 | /** 124 | * Returns the entree's type (file|dir|image|document). 125 | * 126 | * @return string 127 | */ 128 | public function getType(); 129 | 130 | /** 131 | * Returns the file extension. 132 | * 133 | * @return string 134 | */ 135 | public function getExtension(); 136 | 137 | /** 138 | * Returns the entree's directory's path. 139 | * 140 | * @return string 141 | */ 142 | public function getDirname(); 143 | 144 | /** 145 | * Returns the filename. 146 | * 147 | * @param string $suffix If the filename ends in suffix this will also be cut off 148 | * 149 | * @return string 150 | */ 151 | public function getFilename($suffix = null); 152 | 153 | /** 154 | * Returns the entree's timestamp. 155 | * 156 | * @return int unix timestamp 157 | */ 158 | public function getTimestamp(); 159 | 160 | /** 161 | * Returns the entree's timestamp as a Carbon instance. 162 | * 163 | * @return Carbon The Carbon instance. 164 | */ 165 | public function getCarbon(); 166 | 167 | /** 168 | * Returns whether the entree's visibility is public. 169 | * 170 | * @return bool 171 | */ 172 | public function isPublic(); 173 | 174 | /** 175 | * Returns whether the entree's visibility is private. 176 | * 177 | * @return bool 178 | */ 179 | public function isPrivate(); 180 | 181 | /** 182 | * Returns the entree's visibility (public|private). 183 | * 184 | * @return string 185 | */ 186 | public function getVisibility(); 187 | 188 | /** 189 | * Set the visibility. 190 | * 191 | * @param string $visibility One of 'public' or 'private'. 192 | */ 193 | public function setVisibility($visibility); 194 | } 195 | -------------------------------------------------------------------------------- /src/Adapter/Local.php: -------------------------------------------------------------------------------- 1 | applyPathPrefix($path); 46 | $mimetype = Util::guessMimeType($path, $contents); 47 | 48 | if (!is_writable($location)) { 49 | return false; 50 | } 51 | 52 | if (($size = file_put_contents($location, $contents, $this->writeFlags)) === false) { 53 | return false; 54 | } 55 | 56 | $type = 'file'; 57 | 58 | return compact('type', 'path', 'size', 'contents', 'mimetype'); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function delete($path) 65 | { 66 | $location = $this->applyPathPrefix($path); 67 | 68 | if (!file_exists($location)) { 69 | throw new FileNotFoundException($path); 70 | } 71 | 72 | if (!is_writable($location)) { 73 | throw new IOException('File is not writable', $location); 74 | } 75 | 76 | try { 77 | return Thrower::call('unlink', $location); 78 | } catch (\ErrorException $ex) { 79 | if (strpos($ex->getMessage(), "No such file or directory") !== false) { 80 | throw new FileNotFoundException($path, $ex); 81 | } else { 82 | throw new IOException('Could not remove file', $path); 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * {@inheritdoc} 89 | */ 90 | public function createDir($dirname, Config $config) 91 | { 92 | $location = $this->applyPathPrefix($dirname); 93 | $umask = umask(0); 94 | $visibility = $config->get('visibility', 'public'); 95 | 96 | if (!is_dir($location) && !@mkdir($location, $this->permissionMap['dir'][$visibility], true)) { 97 | $return = false; 98 | } else { 99 | $return = ['path' => $dirname, 'type' => 'dir']; 100 | } 101 | 102 | umask($umask); 103 | 104 | return $return; 105 | } 106 | 107 | /** 108 | * {@inheritdoc} 109 | */ 110 | public function deleteDir($dirname) 111 | { 112 | $location = $this->applyPathPrefix($dirname); 113 | if (!is_dir($location) || !is_writable($location)) { 114 | return false; 115 | } 116 | 117 | return parent::deleteDir($dirname); 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function getImageInfo($path) 124 | { 125 | $location = $this->applyPathPrefix($path); 126 | 127 | return Image\Info::createFromFile($location); 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function includeFile($path, $once = true) 134 | { 135 | $location = $this->applyPathPrefix($path); 136 | 137 | try { 138 | return Thrower::call(__NAMESPACE__ . '\includeFile' . ($once ? 'Once' : ''), $location); 139 | } catch (\ErrorException $e) { 140 | throw new IncludeFileException($e->getMessage(), $path, 0, $e); 141 | } 142 | } 143 | 144 | /** 145 | * @inheritdoc 146 | */ 147 | public function getMetadata($path) 148 | { 149 | $location = $this->applyPathPrefix($path); 150 | 151 | if (!file_exists($location)) { 152 | throw new FileNotFoundException($path); 153 | } 154 | 155 | $info = new \SplFileInfo($location); 156 | 157 | return $this->normalizeFileInfo($info); 158 | } 159 | } 160 | 161 | /** 162 | * Scope isolated include. 163 | * 164 | * Prevents access to $this/self from included files. 165 | * 166 | * @param string $file 167 | * 168 | * @return mixed 169 | */ 170 | function includeFile($file) 171 | { 172 | /** @noinspection PhpIncludeInspection */ 173 | return include $file; 174 | } 175 | 176 | /** 177 | * Scope isolated include_once. 178 | * 179 | * Prevents access to $this/self from included files. 180 | * 181 | * @param string $file 182 | * 183 | * @return mixed 184 | */ 185 | function includeFileOnce($file) 186 | { 187 | /** @noinspection PhpIncludeInspection */ 188 | return include_once $file; 189 | } 190 | -------------------------------------------------------------------------------- /src/Iterator/RecursiveDirectoryIterator.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class RecursiveDirectoryIterator implements RecursiveIterator, SeekableIterator 18 | { 19 | /** 20 | * This mode sets keys to the format expected for globbing. 21 | * 22 | * Keys will have a leading slash and directories will have a trailing slash. 23 | * 24 | * Normal key: 25 | * foo_dir 26 | * foo_file 27 | * 28 | * Glob key: 29 | * /foo_dir/ 30 | * /foo_file 31 | */ 32 | const KEY_FOR_GLOB = 1; 33 | 34 | /** @var FilesystemInterface */ 35 | protected $filesystem; 36 | /** @var string */ 37 | protected $path; 38 | /** @var int */ 39 | protected $mode; 40 | 41 | /** @var array */ 42 | protected $contents = []; 43 | /** @var bool */ 44 | protected $fetched = false; 45 | 46 | /** @var array contents for children */ 47 | protected $children = []; 48 | 49 | /** @var int current position */ 50 | protected $position = -1; 51 | /** @var string current path */ 52 | protected $key = null; 53 | /** @var null|Directory|File */ 54 | protected $current = null; 55 | 56 | /** 57 | * Constructor. 58 | * 59 | * @param FilesystemInterface $filesystem 60 | * @param string $path 61 | * @param int $mode 62 | */ 63 | public function __construct(FilesystemInterface $filesystem, $path = '/', $mode = null) 64 | { 65 | $this->filesystem = $filesystem; 66 | $this->path = $path; 67 | $this->mode = $mode; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function valid() 74 | { 75 | return isset($this->contents[$this->position]); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function current() 82 | { 83 | return $this->current; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function key() 90 | { 91 | return $this->key; 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function next() 98 | { 99 | $this->position++; 100 | $this->fetch(); 101 | $this->setCurrent(); 102 | } 103 | 104 | /** 105 | * {@inheritdoc} 106 | */ 107 | public function seek($position) 108 | { 109 | $this->position = $position; 110 | $this->fetch(); 111 | $this->setCurrent(); 112 | } 113 | 114 | /** 115 | * {@inheritdoc} 116 | */ 117 | public function rewind() 118 | { 119 | $this->position = -1; 120 | $this->next(); 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function hasChildren() 127 | { 128 | try { 129 | if (!$this->current || !$this->current->isDir()) { 130 | return false; 131 | } 132 | } catch (FileNotFoundException $e) { 133 | return false; 134 | } 135 | 136 | $path = $this->current->getFullPath(); 137 | if (!isset($this->children[$path])) { 138 | $this->children[$path] = $this->doFetch($path); 139 | } 140 | 141 | return count($this->children[$path]) > 0; 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function getChildren() 148 | { 149 | $path = $this->current->getFullPath(); 150 | $it = new static($this->filesystem, $path); 151 | 152 | $it->contents = $this->children[$path]; 153 | $it->fetched = true; 154 | $it->mode = $this->mode; 155 | 156 | return $it; 157 | } 158 | 159 | /** 160 | * Fetch contents once. 161 | */ 162 | protected function fetch() 163 | { 164 | if (!$this->fetched) { 165 | $this->contents = $this->doFetch($this->path); 166 | $this->fetched = true; 167 | } 168 | } 169 | 170 | /** 171 | * Actually fetch the listing and return it. 172 | * 173 | * @param string $path 174 | * 175 | * @return Directory[]|File[] 176 | */ 177 | protected function doFetch($path) 178 | { 179 | return $this->filesystem->listContents($path); 180 | } 181 | 182 | /** 183 | * Sets the current handler and path. 184 | */ 185 | protected function setCurrent() 186 | { 187 | if (!isset($this->contents[$this->position])) { 188 | $this->current = null; 189 | $this->key = null; 190 | 191 | return; 192 | } 193 | 194 | $this->current = $this->contents[$this->position]; 195 | 196 | $path = $this->current->getFullPath(); 197 | if ($this->mode & static::KEY_FOR_GLOB) { 198 | // Glob code requires absolute paths, so prefix path 199 | // with leading slash, but not before mount point 200 | if (strpos($path, '://') > 0) { 201 | $path = str_replace('://', ':///', $path); 202 | } else { 203 | $path = '/' . ltrim($path, '/'); 204 | } 205 | } 206 | $this->key = $path; 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /tests/Iterator/SortableIteratorTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class SortableIteratorTest extends IteratorTestCase 18 | { 19 | /** @var Filesystem */ 20 | protected $filesystem; 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function setUp() 26 | { 27 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../')); 28 | } 29 | 30 | public function testConstructor() 31 | { 32 | try { 33 | new SortableIterator(new Iterator([]), 'foobar'); 34 | $this->fail('__construct() throws an InvalidArgumentException exception if the mode is not valid'); 35 | } catch (\Exception $e) { 36 | $this->assertInstanceOf('Bolt\Filesystem\Exception\InvalidArgumentException', $e, '__construct() throws an InvalidArgumentException exception if the mode is not valid'); 37 | } 38 | } 39 | 40 | /** 41 | * @dataProvider getAcceptData 42 | */ 43 | public function testAccept($mode, array $expected) 44 | { 45 | $filesystem = $this->getMockBuilder(Filesystem::class) 46 | ->setConstructorArgs([$this->filesystem->getAdapter()]) 47 | ->setMethods(['getTimestamp']) 48 | ->getMock() 49 | ; 50 | $filesystem->method('getTimestamp') 51 | ->willReturnMap([ 52 | ['fixtures/js/script.js', 1], 53 | ['fixtures/css/reset.css', 9], 54 | ['fixtures', 2], 55 | ['fixtures/css/old/old_style.css', 8], 56 | ['fixtures/base.css', 4], 57 | ['fixtures/css/style.css', 7], 58 | ['fixtures/css/old', 5], 59 | ['fixtures/js', 6], 60 | ['fixtures/css', 3], 61 | ]) 62 | ; 63 | 64 | $iterator = new \ArrayIterator([ 65 | new File($filesystem, 'fixtures/js/script.js'), 66 | new File($filesystem, 'fixtures/css/reset.css'), 67 | new File($filesystem, 'fixtures'), 68 | new File($filesystem, 'fixtures/css/old/old_style.css'), 69 | new File($filesystem, 'fixtures/base.css'), 70 | new File($filesystem, 'fixtures/css/style.css'), 71 | new File($filesystem, 'fixtures/css/old'), 72 | new File($filesystem, 'fixtures/js'), 73 | new File($filesystem, 'fixtures/css'), 74 | ]); 75 | 76 | $iterator = new SortableIterator($iterator, $mode); 77 | $this->assertOrderedIterator($expected, $iterator); 78 | } 79 | 80 | public function getAcceptData() 81 | { 82 | return [ 83 | 'sort by name' => [ 84 | SortableIterator::SORT_BY_NAME, 85 | [ 86 | 'fixtures', 87 | 'fixtures/base.css', 88 | 'fixtures/css', 89 | 'fixtures/css/old', 90 | 'fixtures/css/old/old_style.css', 91 | 'fixtures/css/reset.css', 92 | 'fixtures/css/style.css', 93 | 'fixtures/js', 94 | 'fixtures/js/script.js', 95 | ] 96 | ], 97 | 'sort by type' => [ 98 | SortableIterator::SORT_BY_TYPE, 99 | [ 100 | 'fixtures', 101 | 'fixtures/css', 102 | 'fixtures/css/old', 103 | 'fixtures/js', 104 | 'fixtures/base.css', 105 | 'fixtures/css/old/old_style.css', 106 | 'fixtures/css/reset.css', 107 | 'fixtures/css/style.css', 108 | 'fixtures/js/script.js', 109 | ] 110 | ], 111 | 'sort by time' => [ 112 | SortableIterator::SORT_BY_TIME, 113 | [ 114 | 'fixtures/js/script.js', 115 | 'fixtures', 116 | 'fixtures/css', 117 | 'fixtures/base.css', 118 | 'fixtures/css/old', 119 | 'fixtures/js', 120 | 'fixtures/css/style.css', 121 | 'fixtures/css/old/old_style.css', 122 | 'fixtures/css/reset.css', 123 | 124 | ] 125 | ], 126 | 'sort by call' => [ 127 | function (HandlerInterface $a, HandlerInterface $b) { 128 | return strcmp($a->getPath(), $b->getPath()); 129 | }, 130 | [ 131 | 'fixtures', 132 | 'fixtures/base.css', 133 | 'fixtures/css', 134 | 'fixtures/css/old', 135 | 'fixtures/css/old/old_style.css', 136 | 'fixtures/css/reset.css', 137 | 'fixtures/css/style.css', 138 | 'fixtures/js', 139 | 'fixtures/js/script.js', 140 | ] 141 | ], 142 | ]; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Handler/BaseHandler.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | abstract class BaseHandler implements HandlerInterface 18 | { 19 | use MountPointAwareTrait; 20 | 21 | /** @var FilesystemInterface */ 22 | protected $filesystem; 23 | /** @var string */ 24 | protected $path; 25 | 26 | /** 27 | * Constructor. 28 | * 29 | * @param FilesystemInterface $filesystem 30 | * @param null $path 31 | */ 32 | public function __construct(FilesystemInterface $filesystem = null, $path = null) 33 | { 34 | if ($path !== null && !is_string($path)) { 35 | throw new InvalidArgumentException('Path given must be a string.'); 36 | } 37 | 38 | $this->filesystem = $filesystem; 39 | $this->path = $path; 40 | } 41 | 42 | /** 43 | * {@inheritdoc} 44 | */ 45 | public function setFilesystem(FilesystemInterface $filesystem) 46 | { 47 | $this->filesystem = $filesystem; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getFilesystem() 54 | { 55 | return $this->filesystem; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function setPath($path) 62 | { 63 | if (!is_string($path)) { 64 | throw new InvalidArgumentException('Path given must be a string.'); 65 | } 66 | 67 | $this->path = $path; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function getPath() 74 | { 75 | return $this->path; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getFullPath() 82 | { 83 | return (!empty($this->mountPoint) ? $this->mountPoint . '://' : '') . $this->path; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function getParent() 90 | { 91 | return $this->filesystem->getDir($this->getDirname()); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function getExtension() 98 | { 99 | return pathinfo($this->path, PATHINFO_EXTENSION); 100 | } 101 | 102 | /** 103 | * {@inheritdoc} 104 | */ 105 | public function getDirname() 106 | { 107 | return Util::dirname($this->path); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function getFilename($suffix = null) 114 | { 115 | return basename($this->path, $suffix); 116 | } 117 | 118 | /** 119 | * Returns whether the entry exists. 120 | * 121 | * @return bool 122 | */ 123 | public function exists() 124 | { 125 | return $this->filesystem->has($this->path); 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function isDir() 132 | { 133 | return $this->getType() === 'dir'; 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | public function isFile() 140 | { 141 | return !$this->isDir(); 142 | } 143 | 144 | /** 145 | * {@inheritdoc} 146 | */ 147 | public function isImage() 148 | { 149 | return $this->getType() === 'image'; 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function isDocument() 156 | { 157 | return $this->getType() === 'document'; 158 | } 159 | 160 | /** 161 | * {@inheritdoc} 162 | */ 163 | public function getType() 164 | { 165 | return $this->filesystem->getType($this->path); 166 | } 167 | 168 | /** 169 | * {@inheritdoc} 170 | */ 171 | public function getTimestamp() 172 | { 173 | return $this->filesystem->getTimestamp($this->path); 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function getCarbon() 180 | { 181 | return Carbon::createFromTimestamp($this->getTimestamp()); 182 | } 183 | 184 | /** 185 | * @inheritDoc 186 | */ 187 | public function isPublic() 188 | { 189 | return $this->getVisibility() === 'public'; 190 | } 191 | 192 | /** 193 | * @inheritDoc 194 | */ 195 | public function isPrivate() 196 | { 197 | return $this->getVisibility() === 'private'; 198 | } 199 | 200 | /** 201 | * {@inheritdoc} 202 | */ 203 | public function getVisibility() 204 | { 205 | return $this->filesystem->getVisibility($this->path); 206 | } 207 | 208 | /** 209 | * {@inheritdoc} 210 | */ 211 | public function setVisibility($visibility) 212 | { 213 | $this->filesystem->setVisibility($this->path, $visibility); 214 | } 215 | 216 | /** 217 | * Plugins pass-through. 218 | * 219 | * @param string $method 220 | * @param array $arguments 221 | * 222 | * @throws BadMethodCallException 223 | * 224 | * @return mixed 225 | */ 226 | public function __call($method, array $arguments) 227 | { 228 | array_unshift($arguments, $this->path); 229 | $callback = [$this->filesystem, $method]; 230 | 231 | try { 232 | return call_user_func_array($callback, $arguments); 233 | } catch (\BadMethodCallException $e) { 234 | throw new BadMethodCallException( 235 | 'Call to undefined method ' 236 | . get_called_class() 237 | . '::' . $method 238 | ); 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /tests/Iterator/GlobIteratorTest.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem(new Local($this->tempDir)); 23 | 24 | (new Symfony\Filesystem())->mirror($this->rootDir . '/tests/fixtures', $this->tempDir); 25 | } 26 | 27 | public function testIterate() 28 | { 29 | $iterator = new GlobIterator($this->filesystem, '/*.css'); 30 | 31 | $this->assertIterator( 32 | [ 33 | 'base.css', 34 | ], 35 | $iterator 36 | ); 37 | } 38 | 39 | public function testIterateEscaped() 40 | { 41 | if (defined('PHP_WINDOWS_VERSION_MAJOR')) { 42 | $this->markTestSkipped('A "*" in filenames is not supported on Windows.'); 43 | 44 | return; 45 | } 46 | 47 | touch($this->tempDir . '/css/style*.css'); 48 | 49 | $iterator = new GlobIterator($this->filesystem, '/css/style\\*.css'); 50 | 51 | $this->assertIterator( 52 | [ 53 | 'css/style*.css', 54 | ], 55 | $iterator 56 | ); 57 | } 58 | 59 | public function testIterateSpecialChars() 60 | { 61 | if (defined('PHP_WINDOWS_VERSION_MAJOR')) { 62 | $this->markTestSkipped('A "*" in filenames is not supported on Windows.'); 63 | 64 | return; 65 | } 66 | 67 | touch($this->tempDir . '/css/style*.css'); 68 | 69 | $iterator = new GlobIterator($this->filesystem, '/css/style*.css'); 70 | 71 | $this->assertIterator( 72 | [ 73 | 'css/style*.css', 74 | 'css/style.css', 75 | ], 76 | $iterator 77 | ); 78 | } 79 | 80 | public function testIterateDoubleWildcard() 81 | { 82 | $iterator = new GlobIterator($this->filesystem, '/**/*.css'); 83 | 84 | $this->assertIterator( 85 | [ 86 | 'base.css', 87 | 'css/old/old_style.css', 88 | 'css/reset.css', 89 | 'css/style.css', 90 | ], 91 | $iterator 92 | ); 93 | } 94 | 95 | public function testIterateSingleDirectory() 96 | { 97 | $iterator = new GlobIterator($this->filesystem, '/css'); 98 | 99 | $this->assertIterator( 100 | [ 101 | 'css', 102 | ], 103 | $iterator 104 | ); 105 | } 106 | 107 | public function testIterateSingleFile() 108 | { 109 | $iterator = new GlobIterator($this->filesystem, '/css/style.css'); 110 | 111 | $this->assertIterator( 112 | [ 113 | 'css/style.css', 114 | ], 115 | $iterator 116 | ); 117 | } 118 | 119 | public function testIterateSingleFileInDirectoryWithUnreadableFiles() 120 | { 121 | $iterator = new GlobIterator($this->filesystem, ''); 122 | 123 | $this->assertIterator([''], $iterator); 124 | } 125 | 126 | public function testWildcardMayMatchZeroCharacters() 127 | { 128 | $iterator = new GlobIterator($this->filesystem, '/*css'); 129 | 130 | $this->assertIterator( 131 | [ 132 | 'base.css', 133 | 'css', 134 | ], 135 | $iterator 136 | ); 137 | } 138 | 139 | public function testDoubleWildcardMayMatchZeroCharacters() 140 | { 141 | $iterator = new GlobIterator($this->filesystem, '/**/*css'); 142 | 143 | $this->assertIterator( 144 | [ 145 | 'base.css', 146 | 'css', // This one 147 | 'css/old/old_style.css', 148 | 'css/reset.css', 149 | 'css/style.css', 150 | ], 151 | $iterator 152 | ); 153 | } 154 | 155 | public function testWildcardInRoot() 156 | { 157 | $iterator = new GlobIterator($this->filesystem, '/*'); 158 | 159 | $this->assertIterator( 160 | [ 161 | 'base.css', 162 | 'css', 163 | 'images', 164 | 'js', 165 | ], 166 | $iterator 167 | ); 168 | } 169 | 170 | public function testDoubleWildcardInRoot() 171 | { 172 | $iterator = new GlobIterator($this->filesystem, '/**/*'); 173 | 174 | $this->assertIterator( 175 | [ 176 | 'base.css', 177 | 'css', 178 | 'css/old', 179 | 'css/old/old_style.css', 180 | 'css/reset.css', 181 | 'css/style.css', 182 | 'images', 183 | 'images/1-top-left.jpg', 184 | 'images/2-top-right.jpg', 185 | 'images/3-bottom-right.jpg', 186 | 'images/4-bottom-left.jpg', 187 | 'images/5-left-top.jpg', 188 | 'images/6-right-top.jpg', 189 | 'images/7-right-bottom.jpg', 190 | 'images/8-left-bottom.jpg', 191 | 'images/nut.svg', 192 | 'js', 193 | 'js/script.js', 194 | ], 195 | $iterator 196 | ); 197 | } 198 | 199 | public function testNoMatches() 200 | { 201 | $iterator = new GlobIterator($this->filesystem, '/foo*'); 202 | 203 | $this->assertIterator([], $iterator); 204 | } 205 | 206 | public function testNonExistingBaseDirectory() 207 | { 208 | $iterator = new GlobIterator($this->filesystem, '/foo/*'); 209 | 210 | $this->assertIterator([], $iterator); 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Adapter/S3.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class S3 extends AwsS3Adapter 18 | { 19 | /** 20 | * @inheritdoc 21 | * 22 | * Fix to handle bucket for empty paths and directories. 23 | */ 24 | public function getMetadata($path) 25 | { 26 | $dirResult = [ 27 | 'type' => 'dir', 28 | 'path' => $path, 29 | 'timestamp' => 0, 30 | ]; 31 | 32 | $location = $this->applyPathPrefix($path); 33 | 34 | if ($location === '') { 35 | $command = $this->s3Client->getCommand( 36 | 'headBucket', 37 | [ 38 | 'Bucket' => $this->bucket, 39 | ] 40 | ); 41 | } else { 42 | $command = $this->s3Client->getCommand( 43 | 'headObject', 44 | [ 45 | 'Bucket' => $this->bucket, 46 | 'Key' => $location, 47 | ] 48 | ); 49 | } 50 | 51 | /* @var Result $result */ 52 | try { 53 | $result = $this->s3Client->execute($command); 54 | } catch (S3Exception $exception) { 55 | $response = $exception->getResponse(); 56 | 57 | if ($response !== null && $response->getStatusCode() === 404) { 58 | /* 59 | * Path could be a directory. If so, return enough info to treat it as a directory. 60 | * We should really never get here and the path not be a directory, since existence 61 | * has already been verified. 62 | */ 63 | if ($this->doesDirectoryExist($location)) { 64 | return $dirResult; 65 | } 66 | } 67 | 68 | throw $exception; 69 | } 70 | 71 | /* 72 | * Root paths may not throw an exception because: 73 | * - headBucket() always has a valid response (since existence has already been verified). 74 | * - Applying prefix to empty path will result in a trailing slash. If an exception is not 75 | * thrown for this object it is a fake directory (see createDir()). 76 | * 77 | * Both of these cases mean the path is a directory. We return that here, 78 | * because empty paths aren't handled correctly by normalizeResponse. 79 | */ 80 | if ($path === '') { 81 | return $dirResult; 82 | } 83 | 84 | return $this->normalizeResponse($result->toArray(), $path); 85 | } 86 | 87 | /** 88 | * @inheritdoc 89 | * 90 | * Fix to check if bucket existence for empty paths. 91 | */ 92 | public function has($path) 93 | { 94 | $location = $this->applyPathPrefix($path); 95 | 96 | if ($location === '') { 97 | return $this->s3Client->doesBucketExist($this->bucket); 98 | } 99 | 100 | if ($this->s3Client->doesObjectExist($this->bucket, $location)) { 101 | return true; 102 | } 103 | 104 | return $this->doesDirectoryExist($location); 105 | } 106 | 107 | /** 108 | * @inheritdoc 109 | * 110 | * Fix to use "getBucketAcl" if path is empty. 111 | * Also to check if location is directory, and if so 112 | * return "public" since directories don't have ACL. 113 | */ 114 | protected function getRawVisibility($path) 115 | { 116 | $location = $this->applyPathPrefix($path); 117 | if ($location === '') { 118 | $command = $this->s3Client->getCommand( 119 | 'getBucketAcl', 120 | [ 121 | 'Bucket' => $this->bucket, 122 | ] 123 | ); 124 | } else { 125 | $command = $this->s3Client->getCommand( 126 | 'getObjectAcl', 127 | [ 128 | 'Bucket' => $this->bucket, 129 | 'Key' => $location, 130 | ] 131 | ); 132 | } 133 | 134 | try { 135 | $result = $this->s3Client->execute($command); 136 | } catch (S3Exception $e) { 137 | $response = $e->getResponse(); 138 | 139 | if ($response !== null && $response->getStatusCode() === 404) { 140 | /* 141 | * Path could be a directory. If so, return "public" since directories don't have ACL. 142 | * We should really never get here and the path not be a directory, since existence 143 | * has already been verified. 144 | */ 145 | if ($this->doesDirectoryExist($location)) { 146 | return AdapterInterface::VISIBILITY_PUBLIC; 147 | } 148 | } 149 | 150 | throw $e; 151 | } 152 | 153 | /* 154 | * See note in getMetadata(). 155 | * 156 | * TODO We say buckets are always public since we treat them like directories, which we say are public. 157 | * But buckets actually have visibility. Should we use that instead of assuming it is public? 158 | */ 159 | if ($path === '') { 160 | return AdapterInterface::VISIBILITY_PUBLIC; 161 | } 162 | 163 | $visibility = AdapterInterface::VISIBILITY_PRIVATE; 164 | 165 | foreach ($result->get('Grants') as $grant) { 166 | if ( 167 | isset($grant['Grantee']['URI']) 168 | && $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI 169 | && $grant['Permission'] === 'READ' 170 | ) { 171 | $visibility = AdapterInterface::VISIBILITY_PUBLIC; 172 | break; 173 | } 174 | } 175 | 176 | return $visibility; 177 | } 178 | 179 | /** 180 | * @inheritdoc 181 | * 182 | * Fix to return empty string if path and pathPrefix are the same. 183 | */ 184 | public function removePathPrefix($path) 185 | { 186 | $pathPrefix = $this->getPathPrefix(); 187 | 188 | if ($pathPrefix === null) { 189 | return $path; 190 | } 191 | 192 | if ($path === $pathPrefix) { 193 | return ''; 194 | } 195 | 196 | return substr($path, strlen($pathPrefix)); 197 | } 198 | 199 | /** 200 | * @inheritdoc 201 | * 202 | * Only call Util::getStreamSize if $body is a resource. 203 | * Fixed prefix not being removed from response. 204 | * Guess mime type even when body is a resource. 205 | */ 206 | protected function upload($path, $body, Config $config) 207 | { 208 | $key = $this->applyPathPrefix($path); 209 | $options = $this->getOptionsFromConfig($config); 210 | $acl = isset($options['ACL']) ? $options['ACL'] : 'private'; 211 | 212 | if (! isset($options['ContentType'])) { 213 | $options['ContentType'] = Util::guessMimeType($path, $body); 214 | } 215 | 216 | if (! isset($options['ContentLength'])) { 217 | $options['ContentLength'] = is_string($body) ? Util::contentSize($body) : (is_resource($body) ? Util::getStreamSize($body) : null); 218 | } 219 | 220 | if ($options['ContentLength'] === null) { 221 | unset($options['ContentLength']); 222 | } 223 | 224 | $this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]); 225 | 226 | return $this->normalizeResponse($options, $path); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /tests/Handler/FileTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class FileTest extends FilesystemTestCase 17 | { 18 | /** @var FilesystemInterface */ 19 | protected $filesystem; 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function setUp() 25 | { 26 | parent::setUp(); 27 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../')); 28 | } 29 | 30 | public function testConstruct() 31 | { 32 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 33 | $this->assertInstanceOf('Bolt\Filesystem\Handler\File', $file); 34 | 35 | $filesystem = new Filesystem(new Local(__DIR__)); 36 | $file = new File($filesystem); 37 | $this->assertInstanceOf('Bolt\Filesystem\Handler\File', $file); 38 | } 39 | 40 | public function testSetFilesystem() 41 | { 42 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 43 | $filesystem = new Filesystem(new Local(__DIR__)); 44 | $file->setFilesystem($filesystem); 45 | $this->assertInstanceOf('Bolt\Filesystem\Filesystem', $file->getFilesystem()); 46 | } 47 | 48 | public function testGetMimeType() 49 | { 50 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 51 | $this->assertSame('image/jpeg', $file->getMimeType()); 52 | } 53 | 54 | public function testGetVisibility() 55 | { 56 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 57 | $this->assertSame('public', $file->getVisibility()); 58 | } 59 | 60 | public function testGetType() 61 | { 62 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 63 | $this->assertSame('image', $file->getType()); 64 | } 65 | 66 | public function testGetSize() 67 | { 68 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 69 | $this->assertSame(7023, $file->getSize()); 70 | } 71 | 72 | public function testGetSizeFormatted() 73 | { 74 | $file = new File($this->filesystem, 'fixtures/images/2-top-right.jpg'); 75 | $this->assertSame('6.86 KiB', $file->getSizeFormatted()); 76 | $this->assertSame('7.0 KB', $file->getSizeFormatted(true)); 77 | } 78 | 79 | public function testReadStream() 80 | { 81 | $file = new File($this->filesystem, 'fixtures/base.css'); 82 | $stream = $file->readStream(); 83 | $this->assertInstanceOf('Psr\Http\Message\StreamInterface', $stream); 84 | $this->assertRegExp('/koala/', (string) $stream); 85 | $this->assertRegExp('/color: grey;/', (string) $stream); 86 | $this->assertRegExp('/width: 100%/', (string) $stream); 87 | } 88 | 89 | /** 90 | * @expectedException \Bolt\Filesystem\Exception\FileExistsException 91 | * @expectedExceptionMessage File already exists at path: temp/dropbear.log 92 | */ 93 | public function testWrite() 94 | { 95 | $text = 'Attack of the drop bear'; 96 | $file = new File($this->filesystem, 'temp/dropbear.log'); 97 | $file->write($text); 98 | 99 | $newFile = new File($this->filesystem, 'temp/dropbear.log'); 100 | $this->assertSame('Attack of the drop bear', $newFile->read()); 101 | 102 | $newFile->write('anything'); 103 | } 104 | 105 | /** 106 | * @expectedException \Bolt\Filesystem\Exception\FileExistsException 107 | * @expectedExceptionMessage File already exists at path: temp/base.css 108 | */ 109 | public function testWriteStream() 110 | { 111 | $file = new File($this->filesystem, 'fixtures/base.css'); 112 | $stream = $file->readStream(); 113 | 114 | $newFile = new File($this->filesystem, 'temp/base.css'); 115 | $newFile->writeStream($stream); 116 | 117 | $this->assertSame($file->read(), $newFile->read()); 118 | 119 | $newFile->writeStream($stream); 120 | } 121 | 122 | public function testUpdate() 123 | { 124 | $path = 'temp/Spiderbait.txt'; 125 | 126 | $file = new File($this->filesystem, $path); 127 | 128 | $file->write(null); 129 | $file->update('Buy me a pony'); 130 | 131 | $newFile = new File($this->filesystem, $path); 132 | $this->assertSame('Buy me a pony', $newFile->read()); 133 | 134 | $file = new File($this->filesystem, $path); 135 | $file->update('Calypso'); 136 | 137 | $newFile = new File($this->filesystem, $path); 138 | $this->assertSame('Calypso', $newFile->read()); 139 | } 140 | 141 | public function testUpdateStream() 142 | { 143 | $file = new File($this->filesystem, 'fixtures/base.css'); 144 | $stream = $file->readStream(); 145 | 146 | $newFile = new File($this->filesystem, 'temp/koala.css'); 147 | $newFile->write(null); 148 | $newFile->updateStream($stream); 149 | 150 | $this->assertSame($file->read(), $newFile->read()); 151 | } 152 | 153 | public function testPut() 154 | { 155 | $path = 'temp/SilversunPickups.txt'; 156 | 157 | $file = new File($this->filesystem, $path); 158 | $this->assertFalse($file->exists()); 159 | $file->write(null); 160 | $file->put('Nightlight'); 161 | 162 | $newFile = new File($this->filesystem, $path); 163 | $this->assertSame('Nightlight', $newFile->read()); 164 | 165 | $file = new File($this->filesystem, $path); 166 | $this->assertTrue($file->exists()); 167 | $file->put("It's nice to know you work alone"); 168 | 169 | $newFile = new File($this->filesystem, $path); 170 | $this->assertSame("It's nice to know you work alone", $newFile->read()); 171 | } 172 | 173 | public function testPutStream() 174 | { 175 | $file = new File($this->filesystem, 'fixtures/base.css'); 176 | $stream = $file->readStream(); 177 | 178 | $newFile = new File($this->filesystem, 'temp/koala.css'); 179 | $this->assertFalse($newFile->exists()); 180 | $newFile->putStream($stream); 181 | 182 | $this->assertSame($file->read(), $newFile->read()); 183 | } 184 | 185 | public function testRename() 186 | { 187 | $pathOld = 'temp/the-file-formerly-known-as.txt'; 188 | $pathNew = 'temp/the-file.txt'; 189 | 190 | $file = new File($this->filesystem, $pathOld); 191 | $this->assertFalse($file->exists()); 192 | $file->write('Writing tests is so much fun… everyone should do it!'); 193 | $file->rename($pathNew); 194 | 195 | $newFile = new File($this->filesystem, $pathNew); 196 | $this->assertSame('Writing tests is so much fun… everyone should do it!', $newFile->read()); 197 | } 198 | 199 | public function testCopy() 200 | { 201 | $file = new File($this->filesystem, 'fixtures/base.css'); 202 | $file->copy('temp/drop-the-base.css'); 203 | 204 | $newFile = new File($this->filesystem, 'temp/drop-the-base.css'); 205 | $this->assertSame($file->read(), $newFile->read()); 206 | } 207 | 208 | public function testDelete() 209 | { 210 | $file = new File($this->filesystem, 'fixtures/base.css'); 211 | $file->copy('temp/drop-the-base.css'); 212 | 213 | $newFile = new File($this->filesystem, 'temp/drop-the-base.css'); 214 | $this->assertSame($file->read(), $newFile->read()); 215 | $newFile->delete(); 216 | $this->assertFalse($newFile->exists()); 217 | 218 | $newNewFile = new File($this->filesystem, 'temp/drop-the-base.css'); 219 | $this->assertFalse($newNewFile->exists()); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tests/Handler/Image/InfoTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class InfoTest extends TestCase 16 | { 17 | /** @var Filesystem */ 18 | protected $filesystem; 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function setUp() 24 | { 25 | $this->filesystem = new Filesystem(new Local(__DIR__ . '/../../')); 26 | } 27 | 28 | public function testConstruct() 29 | { 30 | $exif = new Image\Exif([]); 31 | $type = Image\Type::getById(IMAGETYPE_JPEG); 32 | new Image\Info(new Image\Dimensions(1024, 768), $type, 2, 7, 'Marcel Marceau', $exif); 33 | } 34 | 35 | public function testCreateFromFile() 36 | { 37 | $file = dirname(dirname(__DIR__)) . '/fixtures/images/1-top-left.jpg'; 38 | $info = Image\Info::createFromFile($file); 39 | 40 | $this->assertInstanceOf(Image\Info::class, $info); 41 | $this->assertInstanceOf(Image\TypeInterface::class, $info->getType()); 42 | $this->assertInstanceOf(Image\Exif::class, $info->getExif()); 43 | 44 | $this->assertSame(400, $info->getWidth()); 45 | $this->assertSame(200, $info->getHeight()); 46 | $this->assertSame(8, $info->getBits()); 47 | $this->assertSame(3, $info->getChannels()); 48 | $this->assertSame('image/jpeg', $info->getMime()); 49 | $this->assertSame(2, $info->getAspectRatio()); 50 | 51 | $this->assertTrue($info->isLandscape()); 52 | $this->assertFalse($info->isPortrait()); 53 | $this->assertFalse($info->isSquare()); 54 | $this->assertTrue($info->isValid()); 55 | } 56 | 57 | public function testCreateFromFileEmpty() 58 | { 59 | $info = Image\Info::createFromFile(__DIR__ . '/../../fixtures2/empty.jpg'); 60 | 61 | $this->assertSame(0, $info->getWidth()); 62 | $this->assertSame(0, $info->getHeight()); 63 | $this->assertSame(0, $info->getBits()); 64 | $this->assertSame(0, $info->getChannels()); 65 | $this->assertSame(null, $info->getMime()); 66 | $this->assertSame(0.0, $info->getAspectRatio()); 67 | $this->assertFalse($info->isValid()); 68 | } 69 | 70 | public function testCreateFromFileInvalid() 71 | { 72 | $info = Image\Info::createFromFile('drop-bear.jpg'); 73 | 74 | $this->assertFalse($info->isValid()); 75 | } 76 | 77 | public function testCreateFromString() 78 | { 79 | $file = $this->filesystem->getFile('fixtures/images/1-top-left.jpg')->read(); 80 | $info = Image\Info::createFromString($file); 81 | 82 | $this->assertInstanceOf(Image\Info::class, $info); 83 | $this->assertInstanceOf(Image\TypeInterface::class, $info->getType()); 84 | $this->assertInstanceOf(Image\Exif::class, $info->getExif()); 85 | 86 | $this->assertSame(400, $info->getWidth()); 87 | $this->assertSame(200, $info->getHeight()); 88 | $this->assertSame(8, $info->getBits()); 89 | $this->assertSame(3, $info->getChannels()); 90 | $this->assertSame('image/jpeg', $info->getMime()); 91 | $this->assertSame(2, $info->getAspectRatio()); 92 | 93 | $this->assertTrue($info->isLandscape()); 94 | $this->assertFalse($info->isPortrait()); 95 | $this->assertFalse($info->isSquare()); 96 | $this->assertTrue($info->isValid()); 97 | } 98 | 99 | public function testCreateFromStringEmpty() 100 | { 101 | $file = $this->filesystem->getFile('fixtures2/empty.jpg'); 102 | 103 | $info = Image\Info::createFromString($file->read(), $file->getPath()); 104 | 105 | $this->assertSame(0, $info->getWidth()); 106 | $this->assertSame(0, $info->getHeight()); 107 | $this->assertSame(0, $info->getBits()); 108 | $this->assertSame(0, $info->getChannels()); 109 | $this->assertSame(null, $info->getMime()); 110 | $this->assertSame(0.0, $info->getAspectRatio()); 111 | $this->assertFalse($info->isValid()); 112 | } 113 | 114 | public function testCreateFromStringInvalid() 115 | { 116 | $info = Image\Info::createFromString('drop-bear.jpg'); 117 | 118 | $this->assertFalse($info->isValid()); 119 | } 120 | 121 | public function testClone() 122 | { 123 | $file = $this->filesystem->getFile('fixtures/images/1-top-left.jpg')->read(); 124 | $info = Image\Info::createFromString($file); 125 | $clone = clone $info; 126 | 127 | $this->assertNotSame($clone->getExif(), $info->getExif()); 128 | } 129 | 130 | public function testSerialize() 131 | { 132 | $file = $this->filesystem->getFile('fixtures/images/1-top-left.jpg')->read(); 133 | $expected = Image\Info::createFromString($file); 134 | /** @var Image\Info $actual */ 135 | $actual = unserialize(serialize($expected)); 136 | 137 | $this->assertInstanceOf(Image\Info::class, $actual); 138 | $this->assertEquals($expected->getDimensions(), $actual->getDimensions()); 139 | $this->assertSame($expected->getType(), $actual->getType()); 140 | $this->assertSame($expected->getBits(), $actual->getBits()); 141 | $this->assertSame($expected->getChannels(), $actual->getChannels()); 142 | $this->assertSame($expected->getMime(), $actual->getMime()); 143 | $this->assertEquals($expected->getExif()->getData(), $actual->getExif()->getData()); 144 | $this->assertSame($expected->isValid(), $actual->isValid()); 145 | } 146 | 147 | public function testJsonSerialize() 148 | { 149 | $file = $this->filesystem->getFile('fixtures/images/1-top-left.jpg')->read(); 150 | $expected = Image\Info::createFromString($file); 151 | $actual = Image\Info::createFromJson(json_decode(json_encode($expected), true)); 152 | 153 | $this->assertEquals($expected->getDimensions(), $actual->getDimensions()); 154 | $this->assertSame($expected->getType(), $actual->getType()); 155 | $this->assertSame($expected->getBits(), $actual->getBits()); 156 | $this->assertSame($expected->getChannels(), $actual->getChannels()); 157 | $this->assertSame($expected->getMime(), $actual->getMime()); 158 | $this->assertEquals($expected->getExif()->getData(), $actual->getExif()->getData()); 159 | $this->assertSame($expected->isValid(), $actual->isValid()); 160 | } 161 | 162 | public function testSvgFromString() 163 | { 164 | $file = $this->filesystem->getFile('fixtures/images/nut.svg')->read(); 165 | $info = Image\Info::createFromString($file); 166 | 167 | $this->assertSame(1000, $info->getWidth()); 168 | $this->assertSame(1000, $info->getHeight()); 169 | $this->assertSame('image/svg+xml', $info->getMime()); 170 | $this->assertTrue($info->isValid()); 171 | $this->assertInstanceOf(Image\SvgType::class, $info->getType()); 172 | } 173 | 174 | public function testSvgFromFile() 175 | { 176 | $info = Image\Info::createFromFile(__DIR__ . '/../../fixtures/images/nut.svg'); 177 | 178 | $this->assertSame(1000, $info->getWidth()); 179 | $this->assertSame(1000, $info->getHeight()); 180 | $this->assertSame('image/svg+xml', $info->getMime()); 181 | $this->assertTrue($info->isValid()); 182 | $this->assertInstanceOf(Image\SvgType::class, $info->getType()); 183 | } 184 | 185 | public function testSvgWithoutXmlDeclaration() 186 | { 187 | $file = $this->filesystem->getFile('fixtures/images/nut.svg'); 188 | $data = $file->read(); 189 | $data = substr($data, 39); 190 | $info = Image\Info::createFromString($data); 191 | 192 | $this->assertSame(1000, $info->getWidth()); 193 | $this->assertSame(1000, $info->getHeight()); 194 | $this->assertSame('image/svg+xml', $info->getMime()); 195 | $this->assertTrue($info->isValid()); 196 | $this->assertInstanceOf(Image\SvgType::class, $info->getType()); 197 | } 198 | 199 | public function testReadExif() 200 | { 201 | $info = Image\Info::createFromFile(__DIR__ . '/../../fixtures2/empty.jpg'); 202 | 203 | $m = new \ReflectionMethod(Image\Info::class,'readExif'); 204 | $m->setAccessible(true); 205 | 206 | $exif = $m->invoke($info, __DIR__ . '/../../fixtures2/empty.jpg'); 207 | $this->assertInstanceOf(Image\Exif::class, $exif); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/FilesystemInterface.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | interface FilesystemInterface extends Capability\ImageInfo, Capability\IncludeFile 23 | { 24 | /** 25 | * Check whether a file exists. 26 | * 27 | * @param string $path The path to the file. 28 | * 29 | * @return bool 30 | */ 31 | public function has($path); 32 | 33 | /** 34 | * Read a file. 35 | * 36 | * @param string $path The path to the file. 37 | * 38 | * @throws FileNotFoundException 39 | * @throws IOException 40 | * 41 | * @return string 42 | */ 43 | public function read($path); 44 | 45 | /** 46 | * Retrieves a read-stream for a path. 47 | * 48 | * @param string $path The path to the file. 49 | * 50 | * @throws FileNotFoundException 51 | * @throws IOException 52 | * 53 | * @return StreamInterface 54 | */ 55 | public function readStream($path); 56 | 57 | /** 58 | * Write a new file. 59 | * 60 | * @param string $path The path of the new file. 61 | * @param string $contents The file contents. 62 | * @param array $config An optional configuration array. 63 | * 64 | * @throws FileExistsException 65 | * @throws IOException 66 | */ 67 | public function write($path, $contents, $config = []); 68 | 69 | /** 70 | * Write a new file using a stream. 71 | * 72 | * @param string $path The path of the new file. 73 | * @param StreamInterface|resource $resource The stream or resource. 74 | * @param array $config An optional configuration array. 75 | * 76 | * @throws InvalidArgumentException If $resource is not a StreamInterface or file handle. 77 | * @throws FileExistsException 78 | * @throws IOException 79 | */ 80 | public function writeStream($path, $resource, $config = []); 81 | 82 | /** 83 | * Update an existing file. 84 | * 85 | * @param string $path The path of the existing file. 86 | * @param string $contents The file contents. 87 | * @param array $config An optional configuration array. 88 | * 89 | * @throws FileNotFoundException 90 | * @throws IOException 91 | */ 92 | public function update($path, $contents, $config = []); 93 | 94 | /** 95 | * Update an existing file using a stream. 96 | * 97 | * @param string $path The path of the existing file. 98 | * @param StreamInterface|resource $resource The stream or resource. 99 | * @param array $config An optional configuration array. 100 | * 101 | * @throws InvalidArgumentException If $resource is not a StreamInterface or file handle. 102 | * @throws FileNotFoundException 103 | * @throws IOException 104 | */ 105 | public function updateStream($path, $resource, $config = []); 106 | 107 | /** 108 | * Create a file or update if exists. 109 | * 110 | * @param string $path The path to the file. 111 | * @param string $contents The file contents. 112 | * @param array $config An optional configuration array. 113 | * 114 | * @throws IOException 115 | */ 116 | public function put($path, $contents, $config = []); 117 | 118 | /** 119 | * Create a file or update if exists. 120 | * 121 | * @param string $path The path to the file. 122 | * @param StreamInterface|resource $resource The stream or resource. 123 | * @param array $config An optional configuration array. 124 | * 125 | * @throws InvalidArgumentException If $resource is not a StreamInterface or file handle. 126 | * @throws IOException 127 | */ 128 | public function putStream($path, $resource, $config = []); 129 | 130 | /** 131 | * Read and delete a file. 132 | * 133 | * @param string $path The path to the file. 134 | * 135 | * @throws FileNotFoundException 136 | * @throws IOException 137 | * 138 | * @return string 139 | */ 140 | public function readAndDelete($path); 141 | 142 | /** 143 | * Rename a file. 144 | * 145 | * @param string $path Path to the existing file. 146 | * @param string $newPath The new path of the file. 147 | * 148 | * @throws FileExistsException Thrown if $newPath exists. 149 | * @throws FileNotFoundException Thrown if $path does not exist. 150 | * @throws IOException 151 | */ 152 | public function rename($path, $newPath); 153 | 154 | /** 155 | * Copy a file. 156 | * 157 | * By default, if the target already exists, it is only overridden if the source is newer. 158 | * 159 | * @param string $origin Path to the original file. 160 | * @param string $target Path to the target file. 161 | * @param bool|null $override Whether to override an existing file. 162 | * true = always override the target. 163 | * false = never override the target. 164 | * null = only override the target if the source is newer. 165 | * 166 | * @throws FileNotFoundException Thrown if $path does not exist. 167 | * @throws IOException 168 | */ 169 | public function copy($origin, $target, $override = null); 170 | 171 | /** 172 | * Delete a file. 173 | * 174 | * @param string $path 175 | * 176 | * @throws FileNotFoundException 177 | * @throws IOException 178 | */ 179 | public function delete($path); 180 | 181 | /** 182 | * Delete a directory. 183 | * 184 | * @param string $dirname 185 | * 186 | * @throws RootViolationException Thrown if $dirname is empty. 187 | * @throws IOException 188 | */ 189 | public function deleteDir($dirname); 190 | 191 | /** 192 | * Create a directory. 193 | * 194 | * @param string $dirname The name of the new directory. 195 | * @param array $config An optional configuration array. 196 | * 197 | * @throws IOException 198 | */ 199 | public function createDir($dirname, $config = []); 200 | 201 | /** 202 | * Copies a directory and its contents to another. 203 | * 204 | * @param string $originDir The origin directory 205 | * @param string $targetDir The target directory 206 | * @param bool|null $override Whether to override an existing file. 207 | * true = always override the target. 208 | * false = never override the target. 209 | * null = only override the target if the source is newer. 210 | */ 211 | public function copyDir($originDir, $targetDir, $override = null); 212 | 213 | /** 214 | * Mirrors a directory to another. 215 | * 216 | * Note: By default, this will delete files in target if they are not in source. 217 | * 218 | * @param string $originDir The origin directory 219 | * @param string $targetDir The target directory 220 | * @param array $config Valid options are: 221 | * - delete = Whether to delete files that are not in the source directory (default: true) 222 | * - override = See {@see copyDir}'s $override parameter for details (default: null) 223 | */ 224 | public function mirror($originDir, $targetDir, $config = []); 225 | 226 | /** 227 | * Get a handler. 228 | * 229 | * @param string $path The path to the file. 230 | * @param HandlerInterface $handler An optional existing handler to populate. 231 | * 232 | * @throws IOException 233 | * 234 | * @return HandlerInterface 235 | */ 236 | public function get($path, HandlerInterface $handler = null); 237 | 238 | /** 239 | * Get a file handler. 240 | * 241 | * @param string $path The path to the file. 242 | * @param FileInterface $handler An optional existing file handler to populate. 243 | * 244 | * @throws IOException 245 | * 246 | * @return FileInterface 247 | */ 248 | public function getFile($path, FileInterface $handler = null); 249 | 250 | /** 251 | * Get a directory handler. 252 | * 253 | * @param string $path The path to the directory. 254 | * 255 | * @throws IOException 256 | * 257 | * @return DirectoryInterface 258 | */ 259 | public function getDir($path); 260 | 261 | /** 262 | * Get a image handler. 263 | * 264 | * @param string $path The path to the file. 265 | * 266 | * @throws IOException 267 | * 268 | * @return ImageInterface 269 | */ 270 | public function getImage($path); 271 | 272 | /** 273 | * Returns the type of the file. 274 | * 275 | * @param string $path The path to the file. 276 | * 277 | * @return string 278 | */ 279 | public function getType($path); 280 | 281 | /** 282 | * Get a file's size. 283 | * 284 | * @param string $path The path to the file. 285 | * 286 | * @throws IOException 287 | * 288 | * @return int 289 | */ 290 | public function getSize($path); 291 | 292 | /** 293 | * Get a file's unix timestamp. 294 | * 295 | * @param string $path The path to the file. 296 | * 297 | * @throws FileNotFoundException 298 | * @throws IOException 299 | * 300 | * @return string 301 | */ 302 | public function getTimestamp($path); 303 | 304 | /** 305 | * Get a file's timestamp as a Carbon instance. 306 | * 307 | * @param string $path The path to the file. 308 | * 309 | * @throws FileNotFoundException 310 | * @throws IOException 311 | * 312 | * @return Carbon 313 | */ 314 | public function getCarbon($path); 315 | 316 | /** 317 | * Get a file's MIME type. 318 | * 319 | * @param string $path The path to the file. 320 | * 321 | * @throws FileNotFoundException 322 | * @throws IOException 323 | * 324 | * @return string 325 | */ 326 | public function getMimeType($path); 327 | 328 | /** 329 | * Get a file's visibility (public|private). 330 | * 331 | * @param string $path The path to the file. 332 | * 333 | * @throws FileNotFoundException 334 | * @throws IOException 335 | * 336 | * @return string 337 | */ 338 | public function getVisibility($path); 339 | 340 | /** 341 | * Set the visibility for a file. 342 | * 343 | * @param string $path The path to the file. 344 | * @param string $visibility One of 'public' or 'private'. 345 | * 346 | * @throws IOException 347 | */ 348 | public function setVisibility($path, $visibility); 349 | 350 | /** 351 | * List contents of a directory. 352 | * 353 | * @param string $directory The directory to list. 354 | * @param bool $recursive Whether to list recursively. 355 | * 356 | * @throws IOException 357 | * 358 | * @return HandlerInterface[] 359 | */ 360 | public function listContents($directory = '', $recursive = false); 361 | 362 | /** 363 | * Returns a finder instance. Let's find some files! 364 | * 365 | * @return Finder 366 | */ 367 | public function find(); 368 | 369 | /** 370 | * Register a plugin. 371 | * 372 | * @param PluginInterface $plugin The plugin to register. 373 | */ 374 | public function addPlugin(PluginInterface $plugin); 375 | } 376 | -------------------------------------------------------------------------------- /src/Handler/Image/Info.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Info implements JsonSerializable, Serializable 21 | { 22 | /** @var Dimensions */ 23 | protected $dimensions; 24 | /** @var TypeInterface */ 25 | protected $type; 26 | /** @var int */ 27 | protected $bits; 28 | /** @var int */ 29 | protected $channels; 30 | /** @var string */ 31 | protected $mime; 32 | /** @var Exif */ 33 | protected $exif; 34 | /** @var bool */ 35 | protected $valid; 36 | 37 | /** @var ReaderInterface */ 38 | protected static $exifReader; 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param Dimensions $dimensions 44 | * @param TypeInterface $type 45 | * @param int $bits 46 | * @param int $channels 47 | * @param string $mime 48 | * @param Exif $exif 49 | */ 50 | public function __construct(Dimensions $dimensions, TypeInterface $type, $bits, $channels, $mime, Exif $exif) 51 | { 52 | $this->dimensions = $dimensions; 53 | $this->type = $type; 54 | $this->bits = (int) $bits; 55 | $this->channels = (int) $channels; 56 | $this->mime = $mime; 57 | $this->exif = $exif; 58 | $this->valid = true; 59 | } 60 | 61 | /** 62 | * Creates an empty Info. Useful for when image does not exists to prevent null checks. 63 | * 64 | * @return Info 65 | * 66 | * @deprecated Use {@see createInvalid} instead. 67 | */ 68 | public static function createEmpty() 69 | { 70 | return static::createInvalid(); 71 | } 72 | 73 | /** 74 | * Creates an empty, invalid Info. Useful to prevent null checks for non-existent or invalid images. 75 | * 76 | * @return Info 77 | */ 78 | public static function createInvalid() 79 | { 80 | $invalid = new static(new Dimensions(0, 0), Type::unknown(), 0, 0, null, new Exif([])); 81 | $invalid->valid = false; 82 | 83 | return $invalid; 84 | } 85 | 86 | /** 87 | * Creates an Info from a file. 88 | * 89 | * @param string $file A filepath 90 | * 91 | * @return Info 92 | */ 93 | public static function createFromFile($file) 94 | { 95 | $info = @getimagesize($file); 96 | if ($info === false) { 97 | $data = @file_get_contents($file); 98 | if ($data === '' || !static::isSvg($data, $file)) { 99 | return static::createInvalid(); 100 | } 101 | 102 | return static::createSvgFromString($data); 103 | } 104 | 105 | $exif = static::readExif($file); 106 | 107 | return static::createFromArray($info, $exif); 108 | } 109 | 110 | /** 111 | * Creates an Info from a string of image data. 112 | * 113 | * @param string $data A string containing the image data 114 | * @param string|null $filename The filename used for determining the MIME Type. 115 | * 116 | * @return Info 117 | */ 118 | public static function createFromString($data, $filename = null) 119 | { 120 | if ($data === '') { 121 | return static::createInvalid(); 122 | } 123 | 124 | if (static::isSvg($data, (string) $filename)) { 125 | return static::createSvgFromString($data); 126 | } 127 | 128 | $info = @getimagesizefromstring($data); 129 | if ($info === false) { 130 | return static::createInvalid(); 131 | } 132 | 133 | $file = sprintf('data://%s;base64,%s', $info['mime'], base64_encode($data)); 134 | $exif = static::readExif($file); 135 | 136 | return static::createFromArray($info, $exif); 137 | } 138 | 139 | /** 140 | * Creates info from a previous json serialized object. 141 | * 142 | * @param array $data 143 | * 144 | * @return Info 145 | */ 146 | public static function createFromJson(array $data) 147 | { 148 | return new static( 149 | new Dimensions($data['dims'][0], $data['dims'][1]), 150 | Type::getById($data['type']), 151 | $data['bits'], 152 | $data['channels'], 153 | $data['mime'], 154 | new Exif($data['exif']) 155 | ); 156 | } 157 | 158 | /** 159 | * @param array $info 160 | * @param Exif $exif 161 | * 162 | * @return Info 163 | */ 164 | protected static function createFromArray(array $info, Exif $exif) 165 | { 166 | // Add defaults to skip isset checks 167 | $info += [ 168 | 0 => 0, 169 | 1 => 0, 170 | 2 => 0, 171 | 'bits' => 0, 172 | 'channels' => 0, 173 | 'mime' => '', 174 | ]; 175 | 176 | return new static( 177 | new Dimensions($info[0], $info[1]), 178 | Type::getById($info[2]), 179 | $info['bits'], 180 | $info['channels'], 181 | $info['mime'], 182 | $exif 183 | ); 184 | } 185 | 186 | /** 187 | * Creates an Info from a string of SVG image data. 188 | * 189 | * @param string $data 190 | * 191 | * @return Info 192 | */ 193 | protected static function createSvgFromString($data) 194 | { 195 | if (!class_exists(SvgImagine::class)) { 196 | throw new LogicException('Cannot parse SVG Image Info without "contao/imagine-svg" library.'); 197 | } 198 | 199 | try { 200 | $image = (new SvgImagine())->load($data); 201 | } catch (RuntimeException $e) { 202 | throw new IOException('Failed to parse image data from string', null, 0, $e); 203 | } 204 | 205 | $box = $image->getSize(); 206 | $dimensions = new Dimensions($box->getWidth(), $box->getHeight()); 207 | 208 | return new static( 209 | $dimensions, 210 | Type::getById(SvgType::ID), 211 | 0, 212 | 0, 213 | SvgType::MIME, 214 | new Exif([]) 215 | ); 216 | } 217 | 218 | /** 219 | * @param string $file 220 | * 221 | * @return Exif 222 | */ 223 | protected static function readExif($file) 224 | { 225 | if (static::$exifReader === null) { 226 | static::$exifReader = Reader::factory(Reader::TYPE_NATIVE); 227 | } 228 | 229 | $exif = static::$exifReader->read($file); 230 | if ($exif instanceof \PHPExif\Exif) { 231 | return Exif::cast($exif); 232 | } 233 | 234 | return new Exif(); 235 | } 236 | 237 | /** 238 | * Determine data string is an SVG image. 239 | * 240 | * @param string $data 241 | * @param string $filename 242 | * 243 | * @return bool 244 | */ 245 | protected static function isSvg($data, $filename) 246 | { 247 | $type = Flysystem\Util::guessMimeType($filename, $data); 248 | 249 | if ($type === SvgType::MIME) { 250 | return true; 251 | } 252 | 253 | // Detect SVG files without the xml declaration (like from Adobe Illustrator) 254 | if (strpos($data, 'dimensions; 270 | } 271 | 272 | /** 273 | * Returns the image width. 274 | * 275 | * @return int 276 | */ 277 | public function getWidth() 278 | { 279 | return $this->dimensions->getWidth(); 280 | } 281 | 282 | /** 283 | * Returns the image height. 284 | * 285 | * @return int 286 | */ 287 | public function getHeight() 288 | { 289 | return $this->dimensions->getHeight(); 290 | } 291 | 292 | /** 293 | * Returns the aspect ratio. 294 | * 295 | * @return float 296 | */ 297 | public function getAspectRatio() 298 | { 299 | if ($this->getWidth() === 0 || $this->getHeight() === 0) { 300 | return 0.0; 301 | } 302 | 303 | // Account for image rotation 304 | if (in_array($this->exif->getOrientation(), [5, 6, 7, 8])) { 305 | return $this->getHeight() / $this->getWidth(); 306 | } 307 | 308 | return $this->getWidth() / $this->getHeight(); 309 | } 310 | 311 | /** 312 | * Returns whether or not the image is landscape. 313 | * 314 | * This is determined by the aspect ratio being 315 | * greater than 5:4. 316 | * 317 | * @return bool 318 | */ 319 | public function isLandscape() 320 | { 321 | return $this->getAspectRatio() >= 1.25; 322 | } 323 | 324 | /** 325 | * Returns whether or not the image is portrait. 326 | * 327 | * This is determined by the aspect ratio being 328 | * less than 4:5. 329 | * 330 | * @return bool 331 | */ 332 | public function isPortrait() 333 | { 334 | return $this->getAspectRatio() <= 0.8; 335 | } 336 | 337 | /** 338 | * Returns whether or not the image is square-ish. 339 | * 340 | * The image is considered square if it is not 341 | * determined to be landscape or portrait. 342 | * 343 | * @return bool 344 | */ 345 | public function isSquare() 346 | { 347 | return !$this->isLandscape() && !$this->isPortrait(); 348 | } 349 | 350 | /** 351 | * Returns the image type. 352 | * 353 | * @return TypeInterface 354 | */ 355 | public function getType() 356 | { 357 | return $this->type; 358 | } 359 | 360 | /** 361 | * Returns the number of bits for each color. 362 | * 363 | * @return int 364 | */ 365 | public function getBits() 366 | { 367 | return $this->bits; 368 | } 369 | 370 | /** 371 | * Returns the number of channels or colors. 372 | * 373 | * 3 for RGB and 4 for CMYK. 374 | * 375 | * @return int 376 | */ 377 | public function getChannels() 378 | { 379 | return $this->channels; 380 | } 381 | 382 | /** 383 | * Returns the image's MIME type. 384 | * 385 | * @return string 386 | */ 387 | public function getMime() 388 | { 389 | return $this->mime; 390 | } 391 | 392 | /** 393 | * Returns the image's EXIF data. 394 | * 395 | * @return Exif 396 | */ 397 | public function getExif() 398 | { 399 | return $this->exif; 400 | } 401 | 402 | /** 403 | * Whether this Info is valid or if there was an error. 404 | * 405 | * @return bool 406 | */ 407 | public function isValid() 408 | { 409 | return $this->valid; 410 | } 411 | 412 | /** 413 | * {@inheritdoc} 414 | */ 415 | public function __clone() 416 | { 417 | $this->exif = clone $this->exif; 418 | } 419 | 420 | /** 421 | * {@inheritdoc} 422 | */ 423 | public function jsonSerialize() 424 | { 425 | return [ 426 | 'dims' => [$this->dimensions->getWidth(), $this->dimensions->getHeight()], 427 | 'type' => $this->type->getId(), 428 | 'bits' => $this->bits, 429 | 'channels' => $this->channels, 430 | 'mime' => $this->mime, 431 | 'exif' => $this->exif->getData(), 432 | 'valid' => $this->valid, 433 | ]; 434 | } 435 | 436 | /** 437 | * {@inheritdoc} 438 | */ 439 | public function serialize() 440 | { 441 | return serialize($this->jsonSerialize()); 442 | } 443 | 444 | /** 445 | * {@inheritdoc} 446 | */ 447 | public function unserialize($serialized) 448 | { 449 | $data = unserialize($serialized); 450 | 451 | $this->dimensions = new Dimensions($data['dims'][0], $data['dims'][1]); 452 | $this->type = Type::getById($data['type']); 453 | $this->bits = $data['bits']; 454 | $this->channels = $data['channels']; 455 | $this->mime = $data['mime']; 456 | $this->exif = new Exif($data['exif']); 457 | $this->valid = $data['valid']; 458 | } 459 | } 460 | --------------------------------------------------------------------------------