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: foreach ($masks as $mask) {
167: $mask = rtrim(strtr($mask, '\\', '/'), '/');
168: $prefix = '';
169: if ($mask === '') {
170: continue;
171:
172: } elseif ($mask === '*') {
173: return NULL;
174:
175: } elseif ($mask[0] === '/') {
176: $mask = ltrim($mask, '/');
177: $prefix = '(?<=^/)';
178: }
179: $pattern[] = $prefix . strtr(preg_quote($mask, '#'),
180: array('\*\*' => '.*', '\*' => '[^/]*', '\?' => '[^/]', '\[\!' => '[^', '\[' => '[', '\]' => ']', '\-' => '-'));
181: }
182: return $pattern ? '#/(' . implode('|', $pattern) . ')\z#i' : NULL;
183: }
184:
185:
186:
187:
188:
189: 190: 191: 192:
193: public function getIterator()
194: {
195: if (!$this->paths) {
196: throw new Nette\InvalidStateException('Call in() or from() to specify directory to search.');
197:
198: } elseif (count($this->paths) === 1) {
199: return $this->buildIterator($this->paths[0]);
200:
201: } else {
202: $iterator = new \AppendIterator();
203: $iterator->append($workaround = new \ArrayIterator(array('workaround PHP bugs #49104, #63077')));
204: foreach ($this->paths as $path) {
205: $iterator->append($this->buildIterator($path));
206: }
207: unset($workaround[0]);
208: return $iterator;
209: }
210: }
211:
212:
213: 214: 215: 216: 217:
218: private function buildIterator($path)
219: {
220: if (PHP_VERSION_ID < 50301) {
221: $iterator = new Nette\Utils\RecursiveDirectoryIteratorFixed($path);
222: } else {
223: $iterator = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
224: }
225:
226: if ($this->exclude) {
227: $filters = $this->exclude;
228: $iterator = new Nette\Iterators\RecursiveFilter($iterator, function($file) use ($filters) {
229: if (!$file->isDot() && !$file->isFile()) {
230: foreach ($filters as $filter) {
231: if (!call_user_func($filter, $file)) {
232: return FALSE;
233: }
234: }
235: }
236: return TRUE;
237: });
238: }
239:
240: if ($this->maxDepth !== 0) {
241: $iterator = new RecursiveIteratorIterator($iterator, $this->order);
242: $iterator->setMaxDepth($this->maxDepth);
243: }
244:
245: if ($this->groups) {
246: $groups = $this->groups;
247: $iterator = new Nette\Iterators\Filter($iterator, function($file) use ($groups) {
248: foreach ($groups as $filters) {
249: foreach ($filters as $filter) {
250: if (!call_user_func($filter, $file)) {
251: continue 2;
252: }
253: }
254: return TRUE;
255: }
256: return FALSE;
257: });
258: }
259:
260: return $iterator;
261: }
262:
263:
264:
265:
266:
267: 268: 269: 270: 271: 272:
273: public function exclude($masks)
274: {
275: if (!is_array($masks)) {
276: $masks = func_get_args();
277: }
278: $pattern = self::buildPattern($masks);
279: if ($pattern) {
280: $this->filter(function($file) use ($pattern) {
281: return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'));
282: });
283: }
284: return $this;
285: }
286:
287:
288: 289: 290: 291: 292:
293: public function filter($callback)
294: {
295: $this->cursor[] = $callback;
296: return $this;
297: }
298:
299:
300: 301: 302: 303: 304:
305: public function limitDepth($depth)
306: {
307: $this->maxDepth = $depth;
308: return $this;
309: }
310:
311:
312: 313: 314: 315: 316: 317:
318: public function size($operator, $size = NULL)
319: {
320: if (func_num_args() === 1) {
321: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) {
322: throw new Nette\InvalidArgumentException('Invalid size predicate format.');
323: }
324: list(, $operator, $size, $unit) = $matches;
325: static $units = array('' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9);
326: $size *= $units[strtolower($unit)];
327: $operator = $operator ? $operator : '=';
328: }
329: return $this->filter(function($file) use ($operator, $size) {
330: return Finder::compare($file->getSize(), $operator, $size);
331: });
332: }
333:
334:
335: 336: 337: 338: 339: 340:
341: public function date($operator, $date = NULL)
342: {
343: if (func_num_args() === 1) {
344: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) {
345: throw new Nette\InvalidArgumentException('Invalid date predicate format.');
346: }
347: list(, $operator, $date) = $matches;
348: $operator = $operator ? $operator : '=';
349: }
350: $date = Nette\DateTime::from($date)->format('U');
351: return $this->filter(function($file) use ($operator, $date) {
352: return Finder::compare($file->getMTime(), $operator, $date);
353: });
354: }
355:
356:
357: 358: 359: 360: 361: 362:
363: public static function compare($l, $operator, $r)
364: {
365: switch ($operator) {
366: case '>':
367: return $l > $r;
368: case '>=':
369: return $l >= $r;
370: case '<':
371: return $l < $r;
372: case '<=':
373: return $l <= $r;
374: case '=':
375: case '==':
376: return $l == $r;
377: case '!':
378: case '!=':
379: case '<>':
380: return $l != $r;
381: default:
382: throw new Nette\InvalidArgumentException("Unknown operator $operator.");
383: }
384: }
385:
386: }
387:
388:
389: if (PHP_VERSION_ID < 50301) {
390:
391: class RecursiveDirectoryIteratorFixed extends \RecursiveDirectoryIterator
392: {
393: function hasChildren()
394: {
395: return parent::hasChildren(TRUE);
396: }
397: }
398: }
399: