1<?php 2 3/** 4 * Hoa 5 * 6 * 7 * @license 8 * 9 * New BSD License 10 * 11 * Copyright © 2007-2017, Hoa community. All rights reserved. 12 * 13 * Redistribution and use in source and binary forms, with or without 14 * modification, are permitted provided that the following conditions are met: 15 * * Redistributions of source code must retain the above copyright 16 * notice, this list of conditions and the following disclaimer. 17 * * Redistributions in binary form must reproduce the above copyright 18 * notice, this list of conditions and the following disclaimer in the 19 * documentation and/or other materials provided with the distribution. 20 * * Neither the name of the Hoa nor the names of its contributors may be 21 * used to endorse or promote products derived from this software without 22 * specific prior written permission. 23 * 24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE 28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 34 * POSSIBILITY OF SUCH DAMAGE. 35 */ 36 37namespace Hoa\File; 38 39use Hoa\Iterator; 40 41/** 42 * Class \Hoa\File\Finder. 43 * 44 * This class allows to find files easily by using filters and flags. 45 * 46 * @copyright Copyright © 2007-2017 Hoa community 47 * @license New BSD License 48 */ 49class Finder implements Iterator\Aggregate 50{ 51 /** 52 * SplFileInfo classname. 53 * 54 * @var string 55 */ 56 protected $_splFileInfo = 'Hoa\File\SplFileInfo'; 57 58 /** 59 * Paths where to look for. 60 * 61 * @var array 62 */ 63 protected $_paths = []; 64 65 /** 66 * Max depth in recursion. 67 * 68 * @var int 69 */ 70 protected $_maxDepth = -1; 71 72 /** 73 * Filters. 74 * 75 * @var array 76 */ 77 protected $_filters = []; 78 79 /** 80 * Flags. 81 * 82 * @var int 83 */ 84 protected $_flags = -1; 85 86 /** 87 * Types of files to handle. 88 * 89 * @var array 90 */ 91 protected $_types = []; 92 93 /** 94 * What comes first: parent or child? 95 * 96 * @var int 97 */ 98 protected $_first = -1; 99 100 /** 101 * Sorts. 102 * 103 * @var array 104 */ 105 protected $_sorts = []; 106 107 108 109 /** 110 * Initialize. 111 * 112 */ 113 public function __construct() 114 { 115 $this->_flags = Iterator\FileSystem::KEY_AS_PATHNAME 116 | Iterator\FileSystem::CURRENT_AS_FILEINFO 117 | Iterator\FileSystem::SKIP_DOTS; 118 $this->_first = Iterator\Recursive\Iterator::SELF_FIRST; 119 120 return; 121 } 122 123 /** 124 * Select a directory to scan. 125 * 126 * @param string|array $paths One or more paths. 127 * @return \Hoa\File\Finder 128 */ 129 public function in($paths) 130 { 131 if (!is_array($paths)) { 132 $paths = [$paths]; 133 } 134 135 foreach ($paths as $path) { 136 if (1 === preg_match('/[\*\?\[\]]/', $path)) { 137 $iterator = new Iterator\CallbackFilter( 138 new Iterator\Glob(rtrim($path, DS)), 139 function ($current) { 140 return $current->isDir(); 141 } 142 ); 143 144 foreach ($iterator as $fileInfo) { 145 $this->_paths[] = $fileInfo->getPathname(); 146 } 147 } else { 148 $this->_paths[] = $path; 149 } 150 } 151 152 return $this; 153 } 154 155 /** 156 * Set max depth for recursion. 157 * 158 * @param int $depth Depth. 159 * @return \Hoa\File\Finder 160 */ 161 public function maxDepth($depth) 162 { 163 $this->_maxDepth = $depth; 164 165 return $this; 166 } 167 168 /** 169 * Include files in the result. 170 * 171 * @return \Hoa\File\Finder 172 */ 173 public function files() 174 { 175 $this->_types[] = 'file'; 176 177 return $this; 178 } 179 180 /** 181 * Include directories in the result. 182 * 183 * @return \Hoa\File\Finder 184 */ 185 public function directories() 186 { 187 $this->_types[] = 'dir'; 188 189 return $this; 190 } 191 192 /** 193 * Include links in the result. 194 * 195 * @return \Hoa\File\Finder 196 */ 197 public function links() 198 { 199 $this->_types[] = 'link'; 200 201 return $this; 202 } 203 204 /** 205 * Follow symbolink links. 206 * 207 * @param bool $flag Whether we follow or not. 208 * @return \Hoa\File\Finder 209 */ 210 public function followSymlinks($flag = true) 211 { 212 if (true === $flag) { 213 $this->_flags ^= Iterator\FileSystem::FOLLOW_SYMLINKS; 214 } else { 215 $this->_flags |= Iterator\FileSystem::FOLLOW_SYMLINKS; 216 } 217 218 return $this; 219 } 220 221 /** 222 * Include files that match a regex. 223 * Example: 224 * $this->name('#\.php$#'); 225 * 226 * @param string $regex Regex. 227 * @return \Hoa\File\Finder 228 */ 229 public function name($regex) 230 { 231 $this->_filters[] = function (\SplFileInfo $current) use ($regex) { 232 return 0 !== preg_match($regex, $current->getBasename()); 233 }; 234 235 return $this; 236 } 237 238 /** 239 * Exclude directories that match a regex. 240 * Example: 241 * $this->notIn('#^\.(git|hg)$#'); 242 * 243 * @param string $regex Regex. 244 * @return \Hoa\File\Finder 245 */ 246 public function notIn($regex) 247 { 248 $this->_filters[] = function (\SplFileInfo $current) use ($regex) { 249 foreach (explode(DS, $current->getPathname()) as $part) { 250 if (0 !== preg_match($regex, $part)) { 251 return false; 252 } 253 } 254 255 return true; 256 }; 257 258 return $this; 259 } 260 261 /** 262 * Include files that respect a certain size. 263 * The size is a string of the form: 264 * operator number unit 265 * where 266 * • operator could be: <, <=, >, >= or =; 267 * • number is a positive integer; 268 * • unit could be: b (default), Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb. 269 * Example: 270 * $this->size('>= 12Kb'); 271 * 272 * @param string $size Size. 273 * @return \Hoa\File\Finder 274 */ 275 public function size($size) 276 { 277 if (0 === preg_match('#^(<|<=|>|>=|=)\s*(\d+)\s*((?:[KMGTPEZY])b)?$#', $size, $matches)) { 278 return $this; 279 } 280 281 $number = floatval($matches[2]); 282 $unit = isset($matches[3]) ? $matches[3] : 'b'; 283 $operator = $matches[1]; 284 285 switch ($unit) { 286 287 case 'b': 288 break; 289 290 // kilo 291 case 'Kb': 292 $number <<= 10; 293 294 break; 295 296 // mega. 297 case 'Mb': 298 $number <<= 20; 299 300 break; 301 302 // giga. 303 case 'Gb': 304 $number <<= 30; 305 306 break; 307 308 // tera. 309 case 'Tb': 310 $number *= 1099511627776; 311 312 break; 313 314 // peta. 315 case 'Pb': 316 $number *= pow(1024, 5); 317 318 break; 319 320 // exa. 321 case 'Eb': 322 $number *= pow(1024, 6); 323 324 break; 325 326 // zetta. 327 case 'Zb': 328 $number *= pow(1024, 7); 329 330 break; 331 332 // yota. 333 case 'Yb': 334 $number *= pow(1024, 8); 335 336 break; 337 } 338 339 $filter = null; 340 341 switch ($operator) { 342 case '<': 343 $filter = function (\SplFileInfo $current) use ($number) { 344 return $current->getSize() < $number; 345 }; 346 347 break; 348 349 case '<=': 350 $filter = function (\SplFileInfo $current) use ($number) { 351 return $current->getSize() <= $number; 352 }; 353 354 break; 355 356 case '>': 357 $filter = function (\SplFileInfo $current) use ($number) { 358 return $current->getSize() > $number; 359 }; 360 361 break; 362 363 case '>=': 364 $filter = function (\SplFileInfo $current) use ($number) { 365 return $current->getSize() >= $number; 366 }; 367 368 break; 369 370 case '=': 371 $filter = function (\SplFileInfo $current) use ($number) { 372 return $current->getSize() === $number; 373 }; 374 375 break; 376 } 377 378 $this->_filters[] = $filter; 379 380 return $this; 381 } 382 383 /** 384 * Whether we should include dots or not (respectively . and ..). 385 * 386 * @param bool $flag Include or not. 387 * @return \Hoa\File\Finder 388 */ 389 public function dots($flag = true) 390 { 391 if (true === $flag) { 392 $this->_flags ^= Iterator\FileSystem::SKIP_DOTS; 393 } else { 394 $this->_flags |= Iterator\FileSystem::SKIP_DOTS; 395 } 396 397 return $this; 398 } 399 400 /** 401 * Include files that are owned by a certain owner. 402 * 403 * @param int $owner Owner. 404 * @return \Hoa\File\Finder 405 */ 406 public function owner($owner) 407 { 408 $this->_filters[] = function (\SplFileInfo $current) use ($owner) { 409 return $current->getOwner() === $owner; 410 }; 411 412 return $this; 413 } 414 415 /** 416 * Format date. 417 * Date can have the following syntax: 418 * date 419 * since date 420 * until date 421 * If the date does not have the “ago” keyword, it will be added. 422 * Example: “42 hours” is equivalent to “since 42 hours” which is equivalent 423 * to “since 42 hours ago”. 424 * 425 * @param string $date Date. 426 * @param int &$operator Operator (-1 for since, 1 for until). 427 * @return int 428 */ 429 protected function formatDate($date, &$operator) 430 { 431 $operator = -1; 432 433 if (0 === preg_match('#\bago\b#', $date)) { 434 $date .= ' ago'; 435 } 436 437 if (0 !== preg_match('#^(since|until)\b(.+)$#', $date, $matches)) { 438 $time = strtotime($matches[2]); 439 440 if ('until' === $matches[1]) { 441 $operator = 1; 442 } 443 } else { 444 $time = strtotime($date); 445 } 446 447 return $time; 448 } 449 450 /** 451 * Include files that have been changed from a certain date. 452 * Example: 453 * $this->changed('since 13 days'); 454 * 455 * @param string $date Date. 456 * @return \Hoa\File\Finder 457 */ 458 public function changed($date) 459 { 460 $time = $this->formatDate($date, $operator); 461 462 if (-1 === $operator) { 463 $this->_filters[] = function (\SplFileInfo $current) use ($time) { 464 return $current->getCTime() >= $time; 465 }; 466 } else { 467 $this->_filters[] = function (\SplFileInfo $current) use ($time) { 468 return $current->getCTime() < $time; 469 }; 470 } 471 472 return $this; 473 } 474 475 /** 476 * Include files that have been modified from a certain date. 477 * Example: 478 * $this->modified('since 13 days'); 479 * 480 * @param string $date Date. 481 * @return \Hoa\File\Finder 482 */ 483 public function modified($date) 484 { 485 $time = $this->formatDate($date, $operator); 486 487 if (-1 === $operator) { 488 $this->_filters[] = function (\SplFileInfo $current) use ($time) { 489 return $current->getMTime() >= $time; 490 }; 491 } else { 492 $this->_filters[] = function (\SplFileInfo $current) use ($time) { 493 return $current->getMTime() < $time; 494 }; 495 } 496 497 return $this; 498 } 499 500 /** 501 * Add your own filter. 502 * The callback will receive 3 arguments: $current, $key and $iterator. It 503 * must return a boolean: true to include the file, false to exclude it. 504 * Example: 505 * // Include files that are readable 506 * $this->filter(function ($current) { 507 * return $current->isReadable(); 508 * }); 509 * 510 * @param callable $callback Callback 511 * @return \Hoa\File\Finder 512 */ 513 public function filter($callback) 514 { 515 $this->_filters[] = $callback; 516 517 return $this; 518 } 519 520 /** 521 * Sort result by name. 522 * If \Collator exists (from ext/intl), the $locale argument will be used 523 * for its constructor. Else, strcmp() will be used. 524 * Example: 525 * $this->sortByName('fr_FR'); 526 * 527 * @param string $locale Locale. 528 * @return \Hoa\File\Finder 529 */ 530 public function sortByName($locale = 'root') 531 { 532 if (true === class_exists('Collator', false)) { 533 $collator = new \Collator($locale); 534 535 $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) use ($collator) { 536 return $collator->compare($a->getPathname(), $b->getPathname()); 537 }; 538 } else { 539 $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { 540 return strcmp($a->getPathname(), $b->getPathname()); 541 }; 542 } 543 544 return $this; 545 } 546 547 /** 548 * Sort result by size. 549 * Example: 550 * $this->sortBySize(); 551 * 552 * @return \Hoa\File\Finder 553 */ 554 public function sortBySize() 555 { 556 $this->_sorts[] = function (\SplFileInfo $a, \SplFileInfo $b) { 557 return $a->getSize() < $b->getSize(); 558 }; 559 560 return $this; 561 } 562 563 /** 564 * Add your own sort. 565 * The callback will receive 2 arguments: $a and $b. Please see the uasort() 566 * function. 567 * Example: 568 * // Sort files by their modified time. 569 * $this->sort(function ($a, $b) { 570 * return $a->getMTime() < $b->getMTime(); 571 * }); 572 * 573 * @param callable $callable Callback. 574 * @return \Hoa\File\Finder 575 */ 576 public function sort($callable) 577 { 578 $this->_sorts[] = $callable; 579 580 return $this; 581 } 582 583 /** 584 * Child comes first when iterating. 585 * 586 * @return \Hoa\File\Finder 587 */ 588 public function childFirst() 589 { 590 $this->_first = Iterator\Recursive\Iterator::CHILD_FIRST; 591 592 return $this; 593 } 594 595 /** 596 * Get the iterator. 597 * 598 * @return \Traversable 599 */ 600 public function getIterator() 601 { 602 $_iterator = new Iterator\Append(); 603 $types = $this->getTypes(); 604 605 if (!empty($types)) { 606 $this->_filters[] = function (\SplFileInfo $current) use ($types) { 607 return in_array($current->getType(), $types); 608 }; 609 } 610 611 $maxDepth = $this->getMaxDepth(); 612 $splFileInfo = $this->getSplFileInfo(); 613 614 foreach ($this->getPaths() as $path) { 615 if (1 == $maxDepth) { 616 $iterator = new Iterator\IteratorIterator( 617 new Iterator\Recursive\Directory( 618 $path, 619 $this->getFlags(), 620 $splFileInfo 621 ), 622 $this->getFirst() 623 ); 624 } else { 625 $iterator = new Iterator\Recursive\Iterator( 626 new Iterator\Recursive\Directory( 627 $path, 628 $this->getFlags(), 629 $splFileInfo 630 ), 631 $this->getFirst() 632 ); 633 634 if (1 < $maxDepth) { 635 $iterator->setMaxDepth($maxDepth - 1); 636 } 637 } 638 639 $_iterator->append($iterator); 640 } 641 642 foreach ($this->getFilters() as $filter) { 643 $_iterator = new Iterator\CallbackFilter( 644 $_iterator, 645 $filter 646 ); 647 } 648 649 $sorts = $this->getSorts(); 650 651 if (empty($sorts)) { 652 return $_iterator; 653 } 654 655 $array = iterator_to_array($_iterator); 656 657 foreach ($sorts as $sort) { 658 uasort($array, $sort); 659 } 660 661 return new Iterator\Map($array); 662 } 663 664 /** 665 * Set SplFileInfo classname. 666 * 667 * @param string $splFileInfo SplFileInfo classname. 668 * @return string 669 */ 670 public function setSplFileInfo($splFileInfo) 671 { 672 $old = $this->_splFileInfo; 673 $this->_splFileInfo = $splFileInfo; 674 675 return $old; 676 } 677 678 /** 679 * Get SplFileInfo classname. 680 * 681 * @return string 682 */ 683 public function getSplFileInfo() 684 { 685 return $this->_splFileInfo; 686 } 687 688 /** 689 * Get all paths. 690 * 691 * @return array 692 */ 693 protected function getPaths() 694 { 695 return $this->_paths; 696 } 697 698 /** 699 * Get max depth. 700 * 701 * @return int 702 */ 703 public function getMaxDepth() 704 { 705 return $this->_maxDepth; 706 } 707 708 /** 709 * Get types. 710 * 711 * @return array 712 */ 713 public function getTypes() 714 { 715 return $this->_types; 716 } 717 718 /** 719 * Get filters. 720 * 721 * @return array 722 */ 723 protected function getFilters() 724 { 725 return $this->_filters; 726 } 727 728 /** 729 * Get sorts. 730 * 731 * @return array 732 */ 733 protected function getSorts() 734 { 735 return $this->_sorts; 736 } 737 738 /** 739 * Get flags. 740 * 741 * @return int 742 */ 743 public function getFlags() 744 { 745 return $this->_flags; 746 } 747 748 /** 749 * Get first. 750 * 751 * @return int 752 */ 753 public function getFirst() 754 { 755 return $this->_first; 756 } 757} 758