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