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