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