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