1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette;
11: use RecursiveDirectoryIterator;
12: use RecursiveIteratorIterator;
13:
14:
15: 16: 17: 18: 19: 20: 21: 22: 23: 24:
25: class Finder implements \IteratorAggregate, \Countable
26: {
27: use Nette\SmartObject;
28:
29:
30: private $paths = [];
31:
32:
33: private $groups = [];
34:
35:
36: private $exclude = [];
37:
38:
39: private $order = RecursiveIteratorIterator::SELF_FIRST;
40:
41:
42: private $maxDepth = -1;
43:
44:
45: private $cursor;
46:
47:
48: 49: 50: 51: 52:
53: public static function find(...$masks)
54: {
55: $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
56: return (new static)->select($masks, 'isDir')->select($masks, 'isFile');
57: }
58:
59:
60: 61: 62: 63: 64:
65: public static function findFiles(...$masks)
66: {
67: $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
68: return (new static)->select($masks, 'isFile');
69: }
70:
71:
72: 73: 74: 75: 76:
77: public static function findDirectories(...$masks)
78: {
79: $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
80: return (new static)->select($masks, 'isDir');
81: }
82:
83:
84: 85: 86: 87: 88: 89:
90: private function select($masks, $type)
91: {
92: $this->cursor = &$this->groups[];
93: $pattern = self::buildPattern($masks);
94: if ($type || $pattern) {
95: $this->filter(function (RecursiveDirectoryIterator $file) use ($type, $pattern) {
96: return !$file->isDot()
97: && (!$type || $file->$type())
98: && (!$pattern || preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/')));
99: });
100: }
101: return $this;
102: }
103:
104:
105: 106: 107: 108: 109:
110: public function in(...$paths)
111: {
112: $this->maxDepth = 0;
113: return $this->from(...$paths);
114: }
115:
116:
117: 118: 119: 120: 121:
122: public function from(...$paths)
123: {
124: if ($this->paths) {
125: throw new Nette\InvalidStateException('Directory to search has already been specified.');
126: }
127: $this->paths = is_array($paths[0]) ? $paths[0] : $paths;
128: $this->cursor = &$this->exclude;
129: return $this;
130: }
131:
132:
133: 134: 135: 136:
137: public function childFirst()
138: {
139: $this->order = RecursiveIteratorIterator::CHILD_FIRST;
140: return $this;
141: }
142:
143:
144: 145: 146: 147: 148:
149: private static function buildPattern($masks)
150: {
151: $pattern = [];
152: foreach ($masks as $mask) {
153: $mask = rtrim(strtr($mask, '\\', '/'), '/');
154: $prefix = '';
155: if ($mask === '') {
156: continue;
157:
158: } elseif ($mask === '*') {
159: return null;
160:
161: } elseif ($mask[0] === '/') {
162: $mask = ltrim($mask, '/');
163: $prefix = '(?<=^/)';
164: }
165: $pattern[] = $prefix . strtr(preg_quote($mask, '#'),
166: ['\*\*' => '.*', '\*' => '[^/]*', '\?' => '[^/]', '\[\!' => '[^', '\[' => '[', '\]' => ']', '\-' => '-']);
167: }
168: return $pattern ? '#/(' . implode('|', $pattern) . ')\z#i' : null;
169: }
170:
171:
172:
173:
174:
175: 176: 177: 178:
179: public function count()
180: {
181: return iterator_count($this->getIterator());
182: }
183:
184:
185: 186: 187: 188:
189: public function getIterator()
190: {
191: if (!$this->paths) {
192: throw new Nette\InvalidStateException('Call in() or from() to specify directory to search.');
193:
194: } elseif (count($this->paths) === 1) {
195: return $this->buildIterator($this->paths[0]);
196:
197: } else {
198: $iterator = new \AppendIterator();
199: $iterator->append($workaround = new \ArrayIterator(['workaround PHP bugs #49104, #63077']));
200: foreach ($this->paths as $path) {
201: $iterator->append($this->buildIterator($path));
202: }
203: unset($workaround[0]);
204: return $iterator;
205: }
206: }
207:
208:
209: 210: 211: 212: 213:
214: private function buildIterator($path)
215: {
216: $iterator = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
217:
218: if ($this->exclude) {
219: $iterator = new \RecursiveCallbackFilterIterator($iterator, function ($foo, $bar, RecursiveDirectoryIterator $file) {
220: if (!$file->isDot() && !$file->isFile()) {
221: foreach ($this->exclude as $filter) {
222: if (!call_user_func($filter, $file)) {
223: return false;
224: }
225: }
226: }
227: return true;
228: });
229: }
230:
231: if ($this->maxDepth !== 0) {
232: $iterator = new RecursiveIteratorIterator($iterator, $this->order);
233: $iterator->setMaxDepth($this->maxDepth);
234: }
235:
236: $iterator = new \CallbackFilterIterator($iterator, function ($foo, $bar, \Iterator $file) {
237: while ($file instanceof \OuterIterator) {
238: $file = $file->getInnerIterator();
239: }
240:
241: foreach ($this->groups as $filters) {
242: foreach ($filters as $filter) {
243: if (!call_user_func($filter, $file)) {
244: continue 2;
245: }
246: }
247: return true;
248: }
249: return false;
250: });
251:
252: return $iterator;
253: }
254:
255:
256:
257:
258:
259: 260: 261: 262: 263: 264:
265: public function exclude(...$masks)
266: {
267: $masks = $masks && is_array($masks[0]) ? $masks[0] : $masks;
268: $pattern = self::buildPattern($masks);
269: if ($pattern) {
270: $this->filter(function (RecursiveDirectoryIterator $file) use ($pattern) {
271: return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'));
272: });
273: }
274: return $this;
275: }
276:
277:
278: 279: 280: 281: 282:
283: public function filter($callback)
284: {
285: $this->cursor[] = $callback;
286: return $this;
287: }
288:
289:
290: 291: 292: 293: 294:
295: public function limitDepth($depth)
296: {
297: $this->maxDepth = $depth;
298: return $this;
299: }
300:
301:
302: 303: 304: 305: 306: 307:
308: public function size($operator, $size = null)
309: {
310: if (func_num_args() === 1) {
311: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) {
312: throw new Nette\InvalidArgumentException('Invalid size predicate format.');
313: }
314: list(, $operator, $size, $unit) = $matches;
315: static $units = ['' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9];
316: $size *= $units[strtolower($unit)];
317: $operator = $operator ?: '=';
318: }
319: return $this->filter(function (RecursiveDirectoryIterator $file) use ($operator, $size) {
320: return self::compare($file->getSize(), $operator, $size);
321: });
322: }
323:
324:
325: 326: 327: 328: 329: 330:
331: public function date($operator, $date = null)
332: {
333: if (func_num_args() === 1) {
334: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) {
335: throw new Nette\InvalidArgumentException('Invalid date predicate format.');
336: }
337: list(, $operator, $date) = $matches;
338: $operator = $operator ?: '=';
339: }
340: $date = DateTime::from($date)->format('U');
341: return $this->filter(function (RecursiveDirectoryIterator $file) use ($operator, $date) {
342: return self::compare($file->getMTime(), $operator, $date);
343: });
344: }
345:
346:
347: 348: 349: 350: 351: 352:
353: public static function compare($l, $operator, $r)
354: {
355: switch ($operator) {
356: case '>':
357: return $l > $r;
358: case '>=':
359: return $l >= $r;
360: case '<':
361: return $l < $r;
362: case '<=':
363: return $l <= $r;
364: case '=':
365: case '==':
366: return $l == $r;
367: case '!':
368: case '!=':
369: case '<>':
370: return $l != $r;
371: default:
372: throw new Nette\InvalidArgumentException("Unknown operator $operator.");
373: }
374: }
375:
376:
377:
378:
379:
380: public function __call($name, $args)
381: {
382: if ($callback = Nette\Utils\ObjectMixin::getExtensionMethod(__CLASS__, $name)) {
383: return $callback($this, ...$args);
384: }
385: Nette\Utils\ObjectMixin::strictCall(__CLASS__, $name);
386: }
387:
388:
389: public static function extensionMethod($name, $callback)
390: {
391: Nette\Utils\ObjectMixin::setExtensionMethod(__CLASS__, $name, $callback);
392: }
393: }
394: