1: <?php
2:
3: 4: 5: 6:
7:
8: namespace Nette\Utils;
9:
10: use Nette;
11: use 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($mask, '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: $iterator = new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::FOLLOW_SYMLINKS);
222:
223: if ($this->exclude) {
224: $filters = $this->exclude;
225: $iterator = new RecursiveCallbackFilterIterator($iterator, function ($foo, $bar, $file) use ($filters) {
226: if (!$file->isDot() && !$file->isFile()) {
227: foreach ($filters as $filter) {
228: if (!call_user_func($filter, $file)) {
229: return FALSE;
230: }
231: }
232: }
233: return TRUE;
234: });
235: }
236:
237: if ($this->maxDepth !== 0) {
238: $iterator = new RecursiveIteratorIterator($iterator, $this->order);
239: $iterator->setMaxDepth($this->maxDepth);
240: }
241:
242: if ($this->groups) {
243: $groups = $this->groups;
244: $iterator = new CallbackFilterIterator($iterator, function ($foo, $bar, $file) use ($groups) {
245: foreach ($groups as $filters) {
246: foreach ($filters as $filter) {
247: if (!call_user_func($filter, $file)) {
248: continue 2;
249: }
250: }
251: return TRUE;
252: }
253: return FALSE;
254: });
255: }
256:
257: return $iterator;
258: }
259:
260:
261:
262:
263:
264: 265: 266: 267: 268: 269:
270: public function exclude($masks)
271: {
272: if (!is_array($masks)) {
273: $masks = func_get_args();
274: }
275: $pattern = self::buildPattern($masks);
276: if ($pattern) {
277: $this->filter(function ($file) use ($pattern) {
278: return !preg_match($pattern, '/' . strtr($file->getSubPathName(), '\\', '/'));
279: });
280: }
281: return $this;
282: }
283:
284:
285: 286: 287: 288: 289:
290: public function filter($callback)
291: {
292: $this->cursor[] = $callback;
293: return $this;
294: }
295:
296:
297: 298: 299: 300: 301:
302: public function limitDepth($depth)
303: {
304: $this->maxDepth = $depth;
305: return $this;
306: }
307:
308:
309: 310: 311: 312: 313: 314:
315: public function size($operator, $size = NULL)
316: {
317: if (func_num_args() === 1) {
318: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?((?:\d*\.)?\d+)\s*(K|M|G|)B?\z#i', $operator, $matches)) {
319: throw new Nette\InvalidArgumentException('Invalid size predicate format.');
320: }
321: list(, $operator, $size, $unit) = $matches;
322: static $units = array('' => 1, 'k' => 1e3, 'm' => 1e6, 'g' => 1e9);
323: $size *= $units[strtolower($unit)];
324: $operator = $operator ? $operator : '=';
325: }
326: return $this->filter(function ($file) use ($operator, $size) {
327: return Finder::compare($file->getSize(), $operator, $size);
328: });
329: }
330:
331:
332: 333: 334: 335: 336: 337:
338: public function date($operator, $date = NULL)
339: {
340: if (func_num_args() === 1) {
341: if (!preg_match('#^(?:([=<>!]=?|<>)\s*)?(.+)\z#i', $operator, $matches)) {
342: throw new Nette\InvalidArgumentException('Invalid date predicate format.');
343: }
344: list(, $operator, $date) = $matches;
345: $operator = $operator ? $operator : '=';
346: }
347: $date = DateTime::from($date)->format('U');
348: return $this->filter(function ($file) use ($operator, $date) {
349: return Finder::compare($file->getMTime(), $operator, $date);
350: });
351: }
352:
353:
354: 355: 356: 357: 358: 359:
360: public static function compare($l, $operator, $r)
361: {
362: switch ($operator) {
363: case '>':
364: return $l > $r;
365: case '>=':
366: return $l >= $r;
367: case '<':
368: return $l < $r;
369: case '<=':
370: return $l <= $r;
371: case '=':
372: case '==':
373: return $l == $r;
374: case '!':
375: case '!=':
376: case '<>':
377: return $l != $r;
378: default:
379: throw new Nette\InvalidArgumentException("Unknown operator $operator.");
380: }
381: }
382:
383: }
384: