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