Buckets:
| /** | |
| * This file is part of the Nette Framework (https://nette.org) | |
| * Copyright (c) 2004 David Grudl (https://davidgrudl.com) | |
| */ | |
| declare(strict_types=1); | |
| namespace Nette\Utils; | |
| use Nette; | |
| /** | |
| * Finder allows searching through directory trees using iterator. | |
| * | |
| * Finder::findFiles('*.php') | |
| * ->size('> 10kB') | |
| * ->from('.') | |
| * ->exclude('temp'); | |
| * | |
| * @implements \IteratorAggregate<string, FileInfo> | |
| */ | |
| class Finder implements \IteratorAggregate | |
| { | |
| use Nette\SmartObject; | |
| /** @var array<array{string, string}> */ | |
| private array $find = []; | |
| /** @var string[] */ | |
| private array $in = []; | |
| /** @var \Closure[] */ | |
| private array $filters = []; | |
| /** @var \Closure[] */ | |
| private array $descentFilters = []; | |
| /** @var array<string|self> */ | |
| private array $appends = []; | |
| private bool $childFirst = false; | |
| /** @var ?callable */ | |
| private $sort; | |
| private int $maxDepth = -1; | |
| private bool $ignoreUnreadableDirs = true; | |
| /** | |
| * Begins search for files and directories matching mask. | |
| */ | |
| public static function find(string|array $masks = ['*']): static | |
| { | |
| $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic | |
| return (new static)->addMask($masks, 'dir')->addMask($masks, 'file'); | |
| } | |
| /** | |
| * Begins search for files matching mask. | |
| */ | |
| public static function findFiles(string|array $masks = ['*']): static | |
| { | |
| $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic | |
| return (new static)->addMask($masks, 'file'); | |
| } | |
| /** | |
| * Begins search for directories matching mask. | |
| */ | |
| public static function findDirectories(string|array $masks = ['*']): static | |
| { | |
| $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic | |
| return (new static)->addMask($masks, 'dir'); | |
| } | |
| /** | |
| * Finds files matching the specified masks. | |
| */ | |
| public function files(string|array $masks = ['*']): static | |
| { | |
| return $this->addMask((array) $masks, 'file'); | |
| } | |
| /** | |
| * Finds directories matching the specified masks. | |
| */ | |
| public function directories(string|array $masks = ['*']): static | |
| { | |
| return $this->addMask((array) $masks, 'dir'); | |
| } | |
| private function addMask(array $masks, string $mode): static | |
| { | |
| foreach ($masks as $mask) { | |
| $mask = FileSystem::unixSlashes($mask); | |
| if ($mode === 'dir') { | |
| $mask = rtrim($mask, '/'); | |
| } | |
| if ($mask === '' || ($mode === 'file' && str_ends_with($mask, '/'))) { | |
| throw new Nette\InvalidArgumentException("Invalid mask '$mask'"); | |
| } | |
| if (str_starts_with($mask, '**/')) { | |
| $mask = substr($mask, 3); | |
| } | |
| $this->find[] = [$mask, $mode]; | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Searches in the given directories. Wildcards are allowed. | |
| */ | |
| public function in(string|array $paths): static | |
| { | |
| $paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic | |
| $this->addLocation($paths, ''); | |
| return $this; | |
| } | |
| /** | |
| * Searches recursively from the given directories. Wildcards are allowed. | |
| */ | |
| public function from(string|array $paths): static | |
| { | |
| $paths = is_array($paths) ? $paths : func_get_args(); // compatibility with variadic | |
| $this->addLocation($paths, '/**'); | |
| return $this; | |
| } | |
| private function addLocation(array $paths, string $ext): void | |
| { | |
| foreach ($paths as $path) { | |
| if ($path === '') { | |
| throw new Nette\InvalidArgumentException("Invalid directory '$path'"); | |
| } | |
| $path = rtrim(FileSystem::unixSlashes($path), '/'); | |
| $this->in[] = $path . $ext; | |
| } | |
| } | |
| /** | |
| * Lists directory's contents before the directory itself. By default, this is disabled. | |
| */ | |
| public function childFirst(bool $state = true): static | |
| { | |
| $this->childFirst = $state; | |
| return $this; | |
| } | |
| /** | |
| * Ignores unreadable directories. By default, this is enabled. | |
| */ | |
| public function ignoreUnreadableDirs(bool $state = true): static | |
| { | |
| $this->ignoreUnreadableDirs = $state; | |
| return $this; | |
| } | |
| /** | |
| * Set a compare function for sorting directory entries. The function will be called to sort entries from the same directory. | |
| * @param callable(FileInfo, FileInfo): int $callback | |
| */ | |
| public function sortBy(callable $callback): static | |
| { | |
| $this->sort = $callback; | |
| return $this; | |
| } | |
| /** | |
| * Sorts files in each directory naturally by name. | |
| */ | |
| public function sortByName(): static | |
| { | |
| $this->sort = fn(FileInfo $a, FileInfo $b): int => strnatcmp($a->getBasename(), $b->getBasename()); | |
| return $this; | |
| } | |
| /** | |
| * Adds the specified paths or appends a new finder that returns. | |
| */ | |
| public function append(string|array|null $paths = null): static | |
| { | |
| if ($paths === null) { | |
| return $this->appends[] = new static; | |
| } | |
| $this->appends = array_merge($this->appends, (array) $paths); | |
| return $this; | |
| } | |
| /********************* filtering ****************d*g**/ | |
| /** | |
| * Skips entries that matches the given masks relative to the ones defined with the in() or from() methods. | |
| */ | |
| public function exclude(string|array $masks): static | |
| { | |
| $masks = is_array($masks) ? $masks : func_get_args(); // compatibility with variadic | |
| foreach ($masks as $mask) { | |
| $mask = FileSystem::unixSlashes($mask); | |
| if (!preg_match('~^/?(\*\*/)?(.+)(/\*\*|/\*|/|)$~D', $mask, $m)) { | |
| throw new Nette\InvalidArgumentException("Invalid mask '$mask'"); | |
| } | |
| $end = $m[3]; | |
| $re = $this->buildPattern($m[2]); | |
| $filter = fn(FileInfo $file): bool => ($end && !$file->isDir()) | |
| || !preg_match($re, FileSystem::unixSlashes($file->getRelativePathname())); | |
| $this->descentFilter($filter); | |
| if ($end !== '/*') { | |
| $this->filter($filter); | |
| } | |
| } | |
| return $this; | |
| } | |
| /** | |
| * Yields only entries which satisfy the given filter. | |
| * @param callable(FileInfo): bool $callback | |
| */ | |
| public function filter(callable $callback): static | |
| { | |
| $this->filters[] = \Closure::fromCallable($callback); | |
| return $this; | |
| } | |
| /** | |
| * It descends only to directories that match the specified filter. | |
| * @param callable(FileInfo): bool $callback | |
| */ | |
| public function descentFilter(callable $callback): static | |
| { | |
| $this->descentFilters[] = \Closure::fromCallable($callback); | |
| return $this; | |
| } | |
| /** | |
| * Sets the maximum depth of entries. | |
| */ | |
| public function limitDepth(?int $depth): static | |
| { | |
| $this->maxDepth = $depth ?? -1; | |
| return $this; | |
| } | |
| /** | |
| * Restricts the search by size. $operator accepts "[operator] [size] [unit]" example: >=10kB | |
| */ | |
| public function size(string $operator, ?int $size = null): static | |
| { | |
| if (func_num_args() === 1) { // in $operator is predicate | |
| if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?$#Di', $operator, $matches)) { | |
| throw new Nette\InvalidArgumentException('Invalid size predicate format.'); | |
| } | |
| [, $operator, $size, $unit] = $matches; | |
| $units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9]; | |
| $size *= $units[strtolower($unit)]; | |
| $operator = $operator ?: '='; | |
| } | |
| return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getSize(), $operator, $size)); | |
| } | |
| /** | |
| * Restricts the search by modified time. $operator accepts "[operator] [date]" example: >1978-01-23 | |
| */ | |
| public function date(string $operator, string|int|\DateTimeInterface|null $date = null): static | |
| { | |
| if (func_num_args() === 1) { // in $operator is predicate | |
| if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)$#Di', $operator, $matches)) { | |
| throw new Nette\InvalidArgumentException('Invalid date predicate format.'); | |
| } | |
| [, $operator, $date] = $matches; | |
| $operator = $operator ?: '='; | |
| } | |
| $date = DateTime::from($date)->format('U'); | |
| return $this->filter(fn(FileInfo $file): bool => !$file->isFile() || Helpers::compare($file->getMTime(), $operator, $date)); | |
| } | |
| /********************* iterator generator ****************d*g**/ | |
| /** | |
| * Returns an array with all found files and directories. | |
| * @return list<FileInfo> | |
| */ | |
| public function collect(): array | |
| { | |
| return iterator_to_array($this->getIterator(), preserve_keys: false); | |
| } | |
| /** @return \Generator<string, FileInfo> */ | |
| public function getIterator(): \Generator | |
| { | |
| $plan = $this->buildPlan(); | |
| foreach ($plan as $dir => $searches) { | |
| yield from $this->traverseDir($dir, $searches); | |
| } | |
| foreach ($this->appends as $item) { | |
| if ($item instanceof self) { | |
| yield from $item->getIterator(); | |
| } else { | |
| $item = FileSystem::platformSlashes($item); | |
| yield $item => new FileInfo($item); | |
| } | |
| } | |
| } | |
| /** | |
| * @param array<object{pattern: string, mode: string, recursive: bool}> $searches | |
| * @param string[] $subdirs | |
| * @return \Generator<string, FileInfo> | |
| */ | |
| private function traverseDir(string $dir, array $searches, array $subdirs = []): \Generator | |
| { | |
| if ($this->maxDepth >= 0 && count($subdirs) > $this->maxDepth) { | |
| return; | |
| } elseif (!is_dir($dir)) { | |
| throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($dir, '/\\'))); | |
| } | |
| try { | |
| $pathNames = new \FilesystemIterator($dir, \FilesystemIterator::FOLLOW_SYMLINKS | \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::UNIX_PATHS); | |
| } catch (\UnexpectedValueException $e) { | |
| if ($this->ignoreUnreadableDirs) { | |
| return; | |
| } else { | |
| throw new Nette\InvalidStateException($e->getMessage()); | |
| } | |
| } | |
| $files = $this->convertToFiles($pathNames, implode('/', $subdirs), FileSystem::isAbsolute($dir)); | |
| if ($this->sort) { | |
| $files = iterator_to_array($files); | |
| usort($files, $this->sort); | |
| } | |
| foreach ($files as $file) { | |
| $pathName = $file->getPathname(); | |
| $cache = $subSearch = []; | |
| if ($file->isDir()) { | |
| foreach ($searches as $search) { | |
| if ($search->recursive && $this->proveFilters($this->descentFilters, $file, $cache)) { | |
| $subSearch[] = $search; | |
| } | |
| } | |
| } | |
| if ($this->childFirst && $subSearch) { | |
| yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()])); | |
| } | |
| $relativePathname = FileSystem::unixSlashes($file->getRelativePathname()); | |
| foreach ($searches as $search) { | |
| if ( | |
| $file->{'is' . $search->mode}() | |
| && preg_match($search->pattern, $relativePathname) | |
| && $this->proveFilters($this->filters, $file, $cache) | |
| ) { | |
| yield $pathName => $file; | |
| break; | |
| } | |
| } | |
| if (!$this->childFirst && $subSearch) { | |
| yield from $this->traverseDir($pathName, $subSearch, array_merge($subdirs, [$file->getBasename()])); | |
| } | |
| } | |
| } | |
| private function convertToFiles(iterable $pathNames, string $relativePath, bool $absolute): \Generator | |
| { | |
| foreach ($pathNames as $pathName) { | |
| if (!$absolute) { | |
| $pathName = preg_replace('~\.?/~A', '', $pathName); | |
| } | |
| $pathName = FileSystem::platformSlashes($pathName); | |
| yield new FileInfo($pathName, $relativePath); | |
| } | |
| } | |
| private function proveFilters(array $filters, FileInfo $file, array &$cache): bool | |
| { | |
| foreach ($filters as $filter) { | |
| $res = &$cache[spl_object_id($filter)]; | |
| $res ??= $filter($file); | |
| if (!$res) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| /** @return array<string, array<object{pattern: string, mode: string, recursive: bool}>> */ | |
| private function buildPlan(): array | |
| { | |
| $plan = $dirCache = []; | |
| foreach ($this->find as [$mask, $mode]) { | |
| $splits = []; | |
| if (FileSystem::isAbsolute($mask)) { | |
| if ($this->in) { | |
| throw new Nette\InvalidStateException("You cannot combine the absolute path in the mask '$mask' and the directory to search '{$this->in[0]}'."); | |
| } | |
| $splits[] = self::splitRecursivePart($mask); | |
| } else { | |
| foreach ($this->in ?: ['.'] as $in) { | |
| $in = strtr($in, ['[' => '[[]', ']' => '[]]']); // in path, do not treat [ and ] as a pattern by glob() | |
| $splits[] = self::splitRecursivePart($in . '/' . $mask); | |
| } | |
| } | |
| foreach ($splits as [$base, $rest, $recursive]) { | |
| $base = $base === '' ? '.' : $base; | |
| $dirs = $dirCache[$base] ??= strpbrk($base, '*?[') | |
| ? glob($base, GLOB_NOSORT | GLOB_ONLYDIR | GLOB_NOESCAPE) | |
| : [strtr($base, ['[[]' => '[', '[]]' => ']'])]; // unescape [ and ] | |
| if (!$dirs) { | |
| throw new Nette\InvalidStateException(sprintf("Directory '%s' does not exist.", rtrim($base, '/\\'))); | |
| } | |
| $search = (object) ['pattern' => $this->buildPattern($rest), 'mode' => $mode, 'recursive' => $recursive]; | |
| foreach ($dirs as $dir) { | |
| $plan[$dir][] = $search; | |
| } | |
| } | |
| } | |
| return $plan; | |
| } | |
| /** | |
| * Since glob() does not know ** wildcard, we divide the path into a part for glob and a part for manual traversal. | |
| */ | |
| private static function splitRecursivePart(string $path): array | |
| { | |
| $a = strrpos($path, '/'); | |
| $parts = preg_split('~(?<=^|/)\*\*($|/)~', substr($path, 0, $a + 1), 2); | |
| return isset($parts[1]) | |
| ? [$parts[0], $parts[1] . substr($path, $a + 1), true] | |
| : [$parts[0], substr($path, $a + 1), false]; | |
| } | |
| /** | |
| * Converts wildcards to regular expression. | |
| */ | |
| private function buildPattern(string $mask): string | |
| { | |
| if ($mask === '*') { | |
| return '##'; | |
| } elseif (str_starts_with($mask, './')) { | |
| $anchor = '^'; | |
| $mask = substr($mask, 2); | |
| } else { | |
| $anchor = '(?:^|/)'; | |
| } | |
| $pattern = strtr( | |
| preg_quote($mask, '#'), | |
| [ | |
| '\*\*/' => '(.+/)?', | |
| '\*' => '[^/]*', | |
| '\?' => '[^/]', | |
| '\[\!' => '[^', | |
| '\[' => '[', | |
| '\]' => ']', | |
| '\-' => '-', | |
| ], | |
| ); | |
| return '#' . $anchor . $pattern . '$#D' . (defined('PHP_WINDOWS_VERSION_BUILD') ? 'i' : ''); | |
| } | |
| } | |
Xet Storage Details
- Size:
- 13.4 kB
- Xet hash:
- c336366e3a27105897d30a5292d456669232fccb14c4ceb438b6c9e46836310b
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.