├── File.php ├── Filesystem.php ├── Folder.php ├── LICENSE.txt ├── README.md └── composer.json /File.php: -------------------------------------------------------------------------------- 1 | Folder = new Folder($splInfo->getPath(), $create, $mode); 91 | if (!is_dir($path)) { 92 | $this->name = ltrim($splInfo->getFilename(), '/\\'); 93 | } 94 | $this->pwd(); 95 | $create && !$this->exists() && $this->safe($path) && $this->create(); 96 | } 97 | 98 | /** 99 | * Closes the current file if it is opened 100 | */ 101 | public function __destruct() 102 | { 103 | $this->close(); 104 | } 105 | 106 | /** 107 | * Creates the file. 108 | * 109 | * @return bool Success 110 | */ 111 | public function create(): bool 112 | { 113 | $dir = $this->Folder->pwd(); 114 | 115 | if (is_dir($dir) && is_writable($dir) && !$this->exists() && touch($this->path)) { 116 | return true; 117 | } 118 | 119 | return false; 120 | } 121 | 122 | /** 123 | * Opens the current file with a given $mode 124 | * 125 | * @param string $mode A valid 'fopen' mode string (r|w|a ...) 126 | * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't 127 | * @return bool True on success, false on failure 128 | */ 129 | public function open(string $mode = 'r', bool $force = false): bool 130 | { 131 | if (!$force && is_resource($this->handle)) { 132 | return true; 133 | } 134 | if ($this->exists() === false && $this->create() === false) { 135 | return false; 136 | } 137 | 138 | $this->handle = fopen($this->path, $mode); 139 | 140 | return is_resource($this->handle); 141 | } 142 | 143 | /** 144 | * Return the contents of this file as a string. 145 | * 146 | * @param string|false $bytes where to start 147 | * @param string $mode A `fread` compatible mode. 148 | * @param bool $force If true then the file will be re-opened even if its already opened, otherwise it won't 149 | * @return string|false String on success, false on failure 150 | */ 151 | public function read($bytes = false, string $mode = 'rb', bool $force = false) 152 | { 153 | if ($bytes === false && $this->lock === null) { 154 | return file_get_contents($this->path); 155 | } 156 | if ($this->open($mode, $force) === false) { 157 | return false; 158 | } 159 | if ($this->lock !== null && flock($this->handle, LOCK_SH) === false) { 160 | return false; 161 | } 162 | if (is_int($bytes)) { 163 | return fread($this->handle, $bytes); 164 | } 165 | 166 | $data = ''; 167 | while (!feof($this->handle)) { 168 | $data .= fgets($this->handle, 4096); 169 | } 170 | 171 | if ($this->lock !== null) { 172 | flock($this->handle, LOCK_UN); 173 | } 174 | if ($bytes === false) { 175 | $this->close(); 176 | } 177 | 178 | return trim($data); 179 | } 180 | 181 | /** 182 | * Sets or gets the offset for the currently opened file. 183 | * 184 | * @param int|false $offset The $offset in bytes to seek. If set to false then the current offset is returned. 185 | * @param int $seek PHP Constant SEEK_SET | SEEK_CUR | SEEK_END determining what the $offset is relative to 186 | * @return int|bool True on success, false on failure (set mode), false on failure 187 | * or integer offset on success (get mode). 188 | */ 189 | public function offset($offset = false, int $seek = SEEK_SET) 190 | { 191 | if ($offset === false) { 192 | if (is_resource($this->handle)) { 193 | return ftell($this->handle); 194 | } 195 | } elseif ($this->open() === true) { 196 | return fseek($this->handle, $offset, $seek) === 0; 197 | } 198 | 199 | return false; 200 | } 201 | 202 | /** 203 | * Prepares an ASCII string for writing. Converts line endings to the 204 | * correct terminator for the current platform. If Windows, "\r\n" will be used, 205 | * all other platforms will use "\n" 206 | * 207 | * @param string $data Data to prepare for writing. 208 | * @param bool $forceWindows If true forces Windows new line string. 209 | * @return string The with converted line endings. 210 | */ 211 | public static function prepare(string $data, bool $forceWindows = false): string 212 | { 213 | $lineBreak = "\n"; 214 | if (DIRECTORY_SEPARATOR === '\\' || $forceWindows === true) { 215 | $lineBreak = "\r\n"; 216 | } 217 | 218 | return strtr($data, ["\r\n" => $lineBreak, "\n" => $lineBreak, "\r" => $lineBreak]); 219 | } 220 | 221 | /** 222 | * Write given data to this file. 223 | * 224 | * @param string $data Data to write to this File. 225 | * @param string $mode Mode of writing. {@link https://secure.php.net/fwrite See fwrite()}. 226 | * @param bool $force Force the file to open 227 | * @return bool Success 228 | */ 229 | public function write(string $data, string $mode = 'w', bool $force = false): bool 230 | { 231 | $success = false; 232 | if ($this->open($mode, $force) === true) { 233 | if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) { 234 | return false; 235 | } 236 | 237 | if (fwrite($this->handle, $data) !== false) { 238 | $success = true; 239 | } 240 | if ($this->lock !== null) { 241 | flock($this->handle, LOCK_UN); 242 | } 243 | } 244 | 245 | return $success; 246 | } 247 | 248 | /** 249 | * Append given data string to this file. 250 | * 251 | * @param string $data Data to write 252 | * @param bool $force Force the file to open 253 | * @return bool Success 254 | */ 255 | public function append(string $data, bool $force = false): bool 256 | { 257 | return $this->write($data, 'a', $force); 258 | } 259 | 260 | /** 261 | * Closes the current file if it is opened. 262 | * 263 | * @return bool True if closing was successful or file was already closed, otherwise false 264 | */ 265 | public function close(): bool 266 | { 267 | if (!is_resource($this->handle)) { 268 | return true; 269 | } 270 | 271 | return fclose($this->handle); 272 | } 273 | 274 | /** 275 | * Deletes the file. 276 | * 277 | * @return bool Success 278 | */ 279 | public function delete(): bool 280 | { 281 | $this->close(); 282 | $this->handle = null; 283 | if ($this->exists()) { 284 | return unlink($this->path); 285 | } 286 | 287 | return false; 288 | } 289 | 290 | /** 291 | * Returns the file info as an array with the following keys: 292 | * 293 | * - dirname 294 | * - basename 295 | * - extension 296 | * - filename 297 | * - filesize 298 | * - mime 299 | * 300 | * @return array File information. 301 | */ 302 | public function info(): array 303 | { 304 | if (!$this->info) { 305 | $this->info = pathinfo($this->path); 306 | } 307 | if (!isset($this->info['filename'])) { 308 | $this->info['filename'] = $this->name(); 309 | } 310 | if (!isset($this->info['filesize'])) { 311 | $this->info['filesize'] = $this->size(); 312 | } 313 | if (!isset($this->info['mime'])) { 314 | $this->info['mime'] = $this->mime(); 315 | } 316 | 317 | return $this->info; 318 | } 319 | 320 | /** 321 | * Returns the file extension. 322 | * 323 | * @return string|false The file extension, false if extension cannot be extracted. 324 | */ 325 | public function ext() 326 | { 327 | if (!$this->info) { 328 | $this->info(); 329 | } 330 | if (isset($this->info['extension'])) { 331 | return $this->info['extension']; 332 | } 333 | 334 | return false; 335 | } 336 | 337 | /** 338 | * Returns the file name without extension. 339 | * 340 | * @return string|false The file name without extension, false if name cannot be extracted. 341 | */ 342 | public function name() 343 | { 344 | if (!$this->info) { 345 | $this->info(); 346 | } 347 | if (isset($this->info['extension'])) { 348 | return static::_basename($this->name, '.' . $this->info['extension']); 349 | } 350 | if ($this->name) { 351 | return $this->name; 352 | } 353 | 354 | return false; 355 | } 356 | 357 | /** 358 | * Returns the file basename. simulate the php basename() for multibyte (mb_basename). 359 | * 360 | * @param string $path Path to file 361 | * @param string|null $ext The name of the extension 362 | * @return string the file basename. 363 | */ 364 | protected static function _basename(string $path, ?string $ext = null): string 365 | { 366 | // check for multibyte string and use basename() if not found 367 | if (mb_strlen($path) === strlen($path)) { 368 | return $ext === null ? basename($path) : basename($path, $ext); 369 | } 370 | 371 | $splInfo = new SplFileInfo($path); 372 | $name = ltrim($splInfo->getFilename(), '/\\'); 373 | 374 | if ($ext === null || $ext === '') { 375 | return $name; 376 | } 377 | $ext = preg_quote($ext); 378 | $new = preg_replace("/({$ext})$/u", '', $name); 379 | 380 | // basename of '/etc/.d' is '.d' not '' 381 | return $new === '' ? $name : $new; 382 | } 383 | 384 | /** 385 | * Makes file name safe for saving 386 | * 387 | * @param string|null $name The name of the file to make safe if different from $this->name 388 | * @param string|null $ext The name of the extension to make safe if different from $this->ext 389 | * @return string The extension of the file 390 | */ 391 | public function safe(?string $name = null, ?string $ext = null): string 392 | { 393 | if (!$name) { 394 | $name = (string)$this->name; 395 | } 396 | if (!$ext) { 397 | $ext = (string)$this->ext(); 398 | } 399 | 400 | return preg_replace("/(?:[^\w\.-]+)/", '_', static::_basename($name, $ext)); 401 | } 402 | 403 | /** 404 | * Get md5 Checksum of file with previous check of Filesize 405 | * 406 | * @param int|true $maxsize in MB or true to force 407 | * @return string|false md5 Checksum {@link https://secure.php.net/md5_file See md5_file()}, 408 | * or false in case of an error. 409 | */ 410 | public function md5($maxsize = 5) 411 | { 412 | if ($maxsize === true) { 413 | return md5_file($this->path); 414 | } 415 | 416 | $size = $this->size(); 417 | if ($size && $size < $maxsize * 1024 * 1024) { 418 | return md5_file($this->path); 419 | } 420 | 421 | return false; 422 | } 423 | 424 | /** 425 | * Returns the full path of the file. 426 | * 427 | * @return string|false Full path to the file, or false on failure 428 | */ 429 | public function pwd() 430 | { 431 | if ($this->path === null) { 432 | $dir = $this->Folder->pwd(); 433 | if ($dir && is_dir($dir)) { 434 | $this->path = $this->Folder->slashTerm($dir) . $this->name; 435 | } 436 | } 437 | 438 | return $this->path; 439 | } 440 | 441 | /** 442 | * Returns true if the file exists. 443 | * 444 | * @return bool True if it exists, false otherwise 445 | */ 446 | public function exists(): bool 447 | { 448 | $this->clearStatCache(); 449 | 450 | return $this->path && file_exists($this->path) && is_file($this->path); 451 | } 452 | 453 | /** 454 | * Returns the "chmod" (permissions) of the file. 455 | * 456 | * @return string|false Permissions for the file, or false in case of an error 457 | */ 458 | public function perms() 459 | { 460 | if ($this->exists()) { 461 | return decoct(fileperms($this->path) & 0777); 462 | } 463 | 464 | return false; 465 | } 466 | 467 | /** 468 | * Returns the file size 469 | * 470 | * @return int|false Size of the file in bytes, or false in case of an error 471 | */ 472 | public function size() 473 | { 474 | if ($this->exists()) { 475 | return filesize($this->path); 476 | } 477 | 478 | return false; 479 | } 480 | 481 | /** 482 | * Returns true if the file is writable. 483 | * 484 | * @return bool True if it's writable, false otherwise 485 | */ 486 | public function writable(): bool 487 | { 488 | return is_writable($this->path); 489 | } 490 | 491 | /** 492 | * Returns true if the File is executable. 493 | * 494 | * @return bool True if it's executable, false otherwise 495 | */ 496 | public function executable(): bool 497 | { 498 | return is_executable($this->path); 499 | } 500 | 501 | /** 502 | * Returns true if the file is readable. 503 | * 504 | * @return bool True if file is readable, false otherwise 505 | */ 506 | public function readable(): bool 507 | { 508 | return is_readable($this->path); 509 | } 510 | 511 | /** 512 | * Returns the file's owner. 513 | * 514 | * @return int|false The file owner, or bool in case of an error 515 | */ 516 | public function owner() 517 | { 518 | if ($this->exists()) { 519 | return fileowner($this->path); 520 | } 521 | 522 | return false; 523 | } 524 | 525 | /** 526 | * Returns the file's group. 527 | * 528 | * @return int|false The file group, or false in case of an error 529 | */ 530 | public function group() 531 | { 532 | if ($this->exists()) { 533 | return filegroup($this->path); 534 | } 535 | 536 | return false; 537 | } 538 | 539 | /** 540 | * Returns last access time. 541 | * 542 | * @return int|false Timestamp of last access time, or false in case of an error 543 | */ 544 | public function lastAccess() 545 | { 546 | if ($this->exists()) { 547 | return fileatime($this->path); 548 | } 549 | 550 | return false; 551 | } 552 | 553 | /** 554 | * Returns last modified time. 555 | * 556 | * @return int|false Timestamp of last modification, or false in case of an error 557 | */ 558 | public function lastChange() 559 | { 560 | if ($this->exists()) { 561 | return filemtime($this->path); 562 | } 563 | 564 | return false; 565 | } 566 | 567 | /** 568 | * Returns the current folder. 569 | * 570 | * @return \Cake\Filesystem\Folder Current folder 571 | */ 572 | public function folder(): Folder 573 | { 574 | return $this->Folder; 575 | } 576 | 577 | /** 578 | * Copy the File to $dest 579 | * 580 | * @param string $dest Absolute path to copy the file to. 581 | * @param bool $overwrite Overwrite $dest if exists 582 | * @return bool Success 583 | */ 584 | public function copy(string $dest, bool $overwrite = true): bool 585 | { 586 | if (!$this->exists() || is_file($dest) && !$overwrite) { 587 | return false; 588 | } 589 | 590 | return copy($this->path, $dest); 591 | } 592 | 593 | /** 594 | * Gets the mime type of the file. Uses the finfo extension if 595 | * it's available, otherwise falls back to mime_content_type(). 596 | * 597 | * @return string|false The mimetype of the file, or false if reading fails. 598 | */ 599 | public function mime() 600 | { 601 | if (!$this->exists()) { 602 | return false; 603 | } 604 | if (class_exists('finfo')) { 605 | $finfo = new finfo(FILEINFO_MIME); 606 | $type = $finfo->file($this->pwd()); 607 | if (!$type) { 608 | return false; 609 | } 610 | [$type] = explode(';', $type); 611 | 612 | return $type; 613 | } 614 | if (function_exists('mime_content_type')) { 615 | return mime_content_type($this->pwd()); 616 | } 617 | 618 | return false; 619 | } 620 | 621 | /** 622 | * Clear PHP's internal stat cache 623 | * 624 | * @param bool $all Clear all cache or not. Passing false will clear 625 | * the stat cache for the current path only. 626 | * @return void 627 | */ 628 | public function clearStatCache($all = false): void 629 | { 630 | if ($all === false && $this->path) { 631 | clearstatcache(true, $this->path); 632 | } 633 | 634 | clearstatcache(); 635 | } 636 | 637 | /** 638 | * Searches for a given text and replaces the text if found. 639 | * 640 | * @param string|array $search Text(s) to search for. 641 | * @param string|array $replace Text(s) to replace with. 642 | * @return bool Success 643 | */ 644 | public function replaceText($search, $replace): bool 645 | { 646 | if (!$this->open('r+')) { 647 | return false; 648 | } 649 | 650 | if ($this->lock !== null && flock($this->handle, LOCK_EX) === false) { 651 | return false; 652 | } 653 | 654 | $replaced = $this->write(str_replace($search, $replace, $this->read()), 'w', true); 655 | 656 | if ($this->lock !== null) { 657 | flock($this->handle, LOCK_UN); 658 | } 659 | $this->close(); 660 | 661 | return $replaced; 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /Filesystem.php: -------------------------------------------------------------------------------- 1 | filterIterator($directory, $filter); 67 | } 68 | 69 | /** 70 | * Find files/ directories recursively in given directory path. 71 | * 72 | * @param string $path Directory path. 73 | * @param mixed $filter If string will be used as regex for filtering using 74 | * `RegexIterator`, if callable will be as callback for `CallbackFilterIterator`. 75 | * Hidden directories (starting with dot e.g. .git) are always skipped. 76 | * @param int|null $flags Flags for FilesystemIterator::__construct(); 77 | * @return \Iterator 78 | */ 79 | public function findRecursive(string $path, $filter = null, ?int $flags = null): Iterator 80 | { 81 | $flags = $flags ?? FilesystemIterator::KEY_AS_PATHNAME 82 | | FilesystemIterator::CURRENT_AS_FILEINFO 83 | | FilesystemIterator::SKIP_DOTS; 84 | $directory = new RecursiveDirectoryIterator($path, $flags); 85 | 86 | $dirFilter = new RecursiveCallbackFilterIterator( 87 | $directory, 88 | function (SplFileInfo $current) { 89 | if ($current->getFilename()[0] === '.' && $current->isDir()) { 90 | return false; 91 | } 92 | 93 | return true; 94 | } 95 | ); 96 | 97 | $flatten = new RecursiveIteratorIterator( 98 | $dirFilter, 99 | RecursiveIteratorIterator::CHILD_FIRST 100 | ); 101 | 102 | if ($filter === null) { 103 | return $flatten; 104 | } 105 | 106 | return $this->filterIterator($flatten, $filter); 107 | } 108 | 109 | /** 110 | * Wrap iterator in additional filtering iterator. 111 | * 112 | * @param \Iterator $iterator Iterator 113 | * @param mixed $filter Regex string or callback. 114 | * @return \Iterator 115 | */ 116 | protected function filterIterator(Iterator $iterator, $filter): Iterator 117 | { 118 | if (is_string($filter)) { 119 | return new RegexIterator($iterator, $filter); 120 | } 121 | 122 | return new CallbackFilterIterator($iterator, $filter); 123 | } 124 | 125 | /** 126 | * Dump contents to file. 127 | * 128 | * @param string $filename File path. 129 | * @param string $content Content to dump. 130 | * @return void 131 | * @throws \Cake\Core\Exception\CakeException When dumping fails. 132 | */ 133 | public function dumpFile(string $filename, string $content): void 134 | { 135 | $dir = dirname($filename); 136 | if (!is_dir($dir)) { 137 | $this->mkdir($dir); 138 | } 139 | 140 | $exists = file_exists($filename); 141 | 142 | if ($this->isStream($filename)) { 143 | // phpcs:ignore 144 | $success = @file_put_contents($filename, $content); 145 | } else { 146 | // phpcs:ignore 147 | $success = @file_put_contents($filename, $content, LOCK_EX); 148 | } 149 | 150 | if ($success === false) { 151 | throw new CakeException(sprintf('Failed dumping content to file `%s`', $dir)); 152 | } 153 | 154 | if (!$exists) { 155 | chmod($filename, 0666 & ~umask()); 156 | } 157 | } 158 | 159 | /** 160 | * Create directory. 161 | * 162 | * @param string $dir Directory path. 163 | * @param int $mode Octal mode passed to mkdir(). Defaults to 0755. 164 | * @return void 165 | * @throws \Cake\Core\Exception\CakeException When directory creation fails. 166 | */ 167 | public function mkdir(string $dir, int $mode = 0755): void 168 | { 169 | if (is_dir($dir)) { 170 | return; 171 | } 172 | 173 | $old = umask(0); 174 | // phpcs:ignore 175 | if (@mkdir($dir, $mode, true) === false) { 176 | umask($old); 177 | throw new CakeException(sprintf('Failed to create directory "%s"', $dir)); 178 | } 179 | 180 | umask($old); 181 | } 182 | 183 | /** 184 | * Delete directory along with all it's contents. 185 | * 186 | * @param string $path Directory path. 187 | * @return bool 188 | * @throws \Cake\Core\Exception\CakeException If path is not a directory. 189 | */ 190 | public function deleteDir(string $path): bool 191 | { 192 | if (!file_exists($path)) { 193 | return true; 194 | } 195 | 196 | if (!is_dir($path)) { 197 | throw new CakeException(sprintf('"%s" is not a directory', $path)); 198 | } 199 | 200 | $iterator = new RecursiveIteratorIterator( 201 | new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS), 202 | RecursiveIteratorIterator::CHILD_FIRST 203 | ); 204 | 205 | $result = true; 206 | foreach ($iterator as $fileInfo) { 207 | $isWindowsLink = DIRECTORY_SEPARATOR === '\\' && $fileInfo->getType() === 'link'; 208 | if ($fileInfo->getType() === self::TYPE_DIR || $isWindowsLink) { 209 | // phpcs:ignore 210 | $result = $result && @rmdir($fileInfo->getPathname()); 211 | unset($fileInfo); 212 | continue; 213 | } 214 | 215 | // phpcs:ignore 216 | $result = $result && @unlink($fileInfo->getPathname()); 217 | // possible inner iterators need to be unset too in order for locks on parents to be released 218 | unset($fileInfo); 219 | } 220 | 221 | // unsetting iterators helps releasing possible locks in certain environments, 222 | // which could otherwise make `rmdir()` fail 223 | unset($iterator); 224 | 225 | // phpcs:ignore 226 | $result = $result && @rmdir($path); 227 | 228 | return $result; 229 | } 230 | 231 | /** 232 | * Copies directory with all it's contents. 233 | * 234 | * @param string $source Source path. 235 | * @param string $destination Destination path. 236 | * @return bool 237 | */ 238 | public function copyDir(string $source, string $destination): bool 239 | { 240 | $destination = (new SplFileInfo($destination))->getPathname(); 241 | 242 | if (!is_dir($destination)) { 243 | $this->mkdir($destination); 244 | } 245 | 246 | $iterator = new FilesystemIterator($source); 247 | 248 | $result = true; 249 | foreach ($iterator as $fileInfo) { 250 | if ($fileInfo->isDir()) { 251 | $result = $result && $this->copyDir( 252 | $fileInfo->getPathname(), 253 | $destination . DIRECTORY_SEPARATOR . $fileInfo->getFilename() 254 | ); 255 | } else { 256 | // phpcs:ignore 257 | $result = $result && @copy( 258 | $fileInfo->getPathname(), 259 | $destination . DIRECTORY_SEPARATOR . $fileInfo->getFilename() 260 | ); 261 | } 262 | } 263 | 264 | return $result; 265 | } 266 | 267 | /** 268 | * Check whether the given path is a stream path. 269 | * 270 | * @param string $path Path. 271 | * @return bool 272 | */ 273 | public function isStream(string $path): bool 274 | { 275 | return strpos($path, '://') !== false; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /Folder.php: -------------------------------------------------------------------------------- 1 | 'getPathname', 102 | self::SORT_TIME => 'getCTime', 103 | ]; 104 | 105 | /** 106 | * Holds messages from last method. 107 | * 108 | * @var array 109 | */ 110 | protected $_messages = []; 111 | 112 | /** 113 | * Holds errors from last method. 114 | * 115 | * @var array 116 | */ 117 | protected $_errors = []; 118 | 119 | /** 120 | * Holds array of complete directory paths. 121 | * 122 | * @var array 123 | */ 124 | protected $_directories; 125 | 126 | /** 127 | * Holds array of complete file paths. 128 | * 129 | * @var array 130 | */ 131 | protected $_files; 132 | 133 | /** 134 | * Constructor. 135 | * 136 | * @param string|null $path Path to folder 137 | * @param bool $create Create folder if not found 138 | * @param int|null $mode Mode (CHMOD) to apply to created folder, false to ignore 139 | */ 140 | public function __construct(?string $path = null, bool $create = false, ?int $mode = null) 141 | { 142 | if (empty($path)) { 143 | $path = TMP; 144 | } 145 | if ($mode) { 146 | $this->mode = $mode; 147 | } 148 | 149 | if (!file_exists($path) && $create === true) { 150 | $this->create($path, $this->mode); 151 | } 152 | if (!Folder::isAbsolute($path)) { 153 | $path = realpath($path); 154 | } 155 | if (!empty($path)) { 156 | $this->cd($path); 157 | } 158 | } 159 | 160 | /** 161 | * Return current path. 162 | * 163 | * @return string|null Current path 164 | */ 165 | public function pwd(): ?string 166 | { 167 | return $this->path; 168 | } 169 | 170 | /** 171 | * Change directory to $path. 172 | * 173 | * @param string $path Path to the directory to change to 174 | * @return string|false The new path. Returns false on failure 175 | */ 176 | public function cd(string $path) 177 | { 178 | $path = $this->realpath($path); 179 | if ($path !== false && is_dir($path)) { 180 | return $this->path = $path; 181 | } 182 | 183 | return false; 184 | } 185 | 186 | /** 187 | * Returns an array of the contents of the current directory. 188 | * The returned array holds two arrays: One of directories and one of files. 189 | * 190 | * @param string|bool $sort Whether you want the results sorted, set this and the sort property 191 | * to false to get unsorted results. 192 | * @param array|bool $exceptions Either an array or boolean true will not grab dot files 193 | * @param bool $fullPath True returns the full path 194 | * @return array Contents of current directory as an array, an empty array on failure 195 | */ 196 | public function read($sort = self::SORT_NAME, $exceptions = false, bool $fullPath = false): array 197 | { 198 | $dirs = $files = []; 199 | 200 | if (!$this->pwd()) { 201 | return [$dirs, $files]; 202 | } 203 | if (is_array($exceptions)) { 204 | $exceptions = array_flip($exceptions); 205 | } 206 | $skipHidden = isset($exceptions['.']) || $exceptions === true; 207 | 208 | try { 209 | $iterator = new DirectoryIterator($this->path); 210 | } catch (Exception $e) { 211 | return [$dirs, $files]; 212 | } 213 | 214 | if (!is_bool($sort) && isset($this->_fsorts[$sort])) { 215 | $methodName = $this->_fsorts[$sort]; 216 | } else { 217 | $methodName = $this->_fsorts[self::SORT_NAME]; 218 | } 219 | 220 | foreach ($iterator as $item) { 221 | if ($item->isDot()) { 222 | continue; 223 | } 224 | $name = $item->getFilename(); 225 | if ($skipHidden && $name[0] === '.' || isset($exceptions[$name])) { 226 | continue; 227 | } 228 | if ($fullPath) { 229 | $name = $item->getPathname(); 230 | } 231 | 232 | if ($item->isDir()) { 233 | $dirs[$item->{$methodName}()][] = $name; 234 | } else { 235 | $files[$item->{$methodName}()][] = $name; 236 | } 237 | } 238 | 239 | if ($sort || $this->sort) { 240 | ksort($dirs); 241 | ksort($files); 242 | } 243 | 244 | if ($dirs) { 245 | $dirs = array_merge(...array_values($dirs)); 246 | } 247 | 248 | if ($files) { 249 | $files = array_merge(...array_values($files)); 250 | } 251 | 252 | return [$dirs, $files]; 253 | } 254 | 255 | /** 256 | * Returns an array of all matching files in current directory. 257 | * 258 | * @param string $regexpPattern Preg_match pattern (Defaults to: .*) 259 | * @param string|bool $sort Whether results should be sorted. 260 | * @return array Files that match given pattern 261 | */ 262 | public function find(string $regexpPattern = '.*', $sort = false): array 263 | { 264 | [, $files] = $this->read($sort); 265 | 266 | return array_values(preg_grep('/^' . $regexpPattern . '$/i', $files)); 267 | } 268 | 269 | /** 270 | * Returns an array of all matching files in and below current directory. 271 | * 272 | * @param string $pattern Preg_match pattern (Defaults to: .*) 273 | * @param string|bool $sort Whether results should be sorted. 274 | * @return array Files matching $pattern 275 | */ 276 | public function findRecursive(string $pattern = '.*', $sort = false): array 277 | { 278 | if (!$this->pwd()) { 279 | return []; 280 | } 281 | $startsOn = $this->path; 282 | $out = $this->_findRecursive($pattern, $sort); 283 | $this->cd($startsOn); 284 | 285 | return $out; 286 | } 287 | 288 | /** 289 | * Private helper function for findRecursive. 290 | * 291 | * @param string $pattern Pattern to match against 292 | * @param bool $sort Whether results should be sorted. 293 | * @return array Files matching pattern 294 | */ 295 | protected function _findRecursive(string $pattern, bool $sort = false): array 296 | { 297 | [$dirs, $files] = $this->read($sort); 298 | $found = []; 299 | 300 | foreach ($files as $file) { 301 | if (preg_match('/^' . $pattern . '$/i', $file)) { 302 | $found[] = Folder::addPathElement($this->path, $file); 303 | } 304 | } 305 | $start = $this->path; 306 | 307 | foreach ($dirs as $dir) { 308 | $this->cd(Folder::addPathElement($start, $dir)); 309 | $found = array_merge($found, $this->findRecursive($pattern, $sort)); 310 | } 311 | 312 | return $found; 313 | } 314 | 315 | /** 316 | * Returns true if given $path is a Windows path. 317 | * 318 | * @param string $path Path to check 319 | * @return bool true if windows path, false otherwise 320 | */ 321 | public static function isWindowsPath(string $path): bool 322 | { 323 | return preg_match('/^[A-Z]:\\\\/i', $path) || substr($path, 0, 2) === '\\\\'; 324 | } 325 | 326 | /** 327 | * Returns true if given $path is an absolute path. 328 | * 329 | * @param string $path Path to check 330 | * @return bool true if path is absolute. 331 | */ 332 | public static function isAbsolute(string $path): bool 333 | { 334 | if (empty($path)) { 335 | return false; 336 | } 337 | 338 | return $path[0] === '/' || 339 | preg_match('/^[A-Z]:\\\\/i', $path) || 340 | substr($path, 0, 2) === '\\\\' || 341 | self::isRegisteredStreamWrapper($path); 342 | } 343 | 344 | /** 345 | * Returns true if given $path is a registered stream wrapper. 346 | * 347 | * @param string $path Path to check 348 | * @return bool True if path is registered stream wrapper. 349 | */ 350 | public static function isRegisteredStreamWrapper(string $path): bool 351 | { 352 | return preg_match('/^[^:\/\/]+?(?=:\/\/)/', $path, $matches) && 353 | in_array($matches[0], stream_get_wrappers(), true); 354 | } 355 | 356 | /** 357 | * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) 358 | * 359 | * @param string $path Path to transform 360 | * @return string Path with the correct set of slashes ("\\" or "/") 361 | */ 362 | public static function normalizeFullPath(string $path): string 363 | { 364 | $to = Folder::correctSlashFor($path); 365 | $from = ($to === '/' ? '\\' : '/'); 366 | 367 | return str_replace($from, $to, $path); 368 | } 369 | 370 | /** 371 | * Returns a correct set of slashes for given $path. (\\ for Windows paths and / for other paths.) 372 | * 373 | * @param string $path Path to check 374 | * @return string Set of slashes ("\\" or "/") 375 | */ 376 | public static function correctSlashFor(string $path): string 377 | { 378 | return Folder::isWindowsPath($path) ? '\\' : '/'; 379 | } 380 | 381 | /** 382 | * Returns $path with added terminating slash (corrected for Windows or other OS). 383 | * 384 | * @param string $path Path to check 385 | * @return string Path with ending slash 386 | */ 387 | public static function slashTerm(string $path): string 388 | { 389 | if (Folder::isSlashTerm($path)) { 390 | return $path; 391 | } 392 | 393 | return $path . Folder::correctSlashFor($path); 394 | } 395 | 396 | /** 397 | * Returns $path with $element added, with correct slash in-between. 398 | * 399 | * @param string $path Path 400 | * @param string|array $element Element to add at end of path 401 | * @return string Combined path 402 | */ 403 | public static function addPathElement(string $path, $element): string 404 | { 405 | $element = (array)$element; 406 | array_unshift($element, rtrim($path, DIRECTORY_SEPARATOR)); 407 | 408 | return implode(DIRECTORY_SEPARATOR, $element); 409 | } 410 | 411 | /** 412 | * Returns true if the Folder is in the given path. 413 | * 414 | * @param string $path The absolute path to check that the current `pwd()` resides within. 415 | * @param bool $reverse Reverse the search, check if the given `$path` resides within the current `pwd()`. 416 | * @return bool 417 | * @throws \InvalidArgumentException When the given `$path` argument is not an absolute path. 418 | */ 419 | public function inPath(string $path, bool $reverse = false): bool 420 | { 421 | if (!Folder::isAbsolute($path)) { 422 | throw new InvalidArgumentException('The $path argument is expected to be an absolute path.'); 423 | } 424 | 425 | $dir = Folder::slashTerm($path); 426 | $current = Folder::slashTerm($this->pwd()); 427 | 428 | if (!$reverse) { 429 | $return = preg_match('/^' . preg_quote($dir, '/') . '(.*)/', $current); 430 | } else { 431 | $return = preg_match('/^' . preg_quote($current, '/') . '(.*)/', $dir); 432 | } 433 | 434 | return (bool)$return; 435 | } 436 | 437 | /** 438 | * Change the mode on a directory structure recursively. This includes changing the mode on files as well. 439 | * 440 | * @param string $path The path to chmod. 441 | * @param int|null $mode Octal value, e.g. 0755. 442 | * @param bool $recursive Chmod recursively, set to false to only change the current directory. 443 | * @param string[] $exceptions Array of files, directories to skip. 444 | * @return bool Success. 445 | */ 446 | public function chmod(string $path, ?int $mode = null, bool $recursive = true, array $exceptions = []): bool 447 | { 448 | if (!$mode) { 449 | $mode = $this->mode; 450 | } 451 | 452 | if ($recursive === false && is_dir($path)) { 453 | // phpcs:disable 454 | if (@chmod($path, intval($mode, 8))) { 455 | // phpcs:enable 456 | $this->_messages[] = sprintf('%s changed to %s', $path, $mode); 457 | 458 | return true; 459 | } 460 | 461 | $this->_errors[] = sprintf('%s NOT changed to %s', $path, $mode); 462 | 463 | return false; 464 | } 465 | 466 | if (is_dir($path)) { 467 | $paths = $this->tree($path); 468 | 469 | foreach ($paths as $type) { 470 | foreach ($type as $fullpath) { 471 | $check = explode(DIRECTORY_SEPARATOR, $fullpath); 472 | $count = count($check); 473 | 474 | if (in_array($check[$count - 1], $exceptions, true)) { 475 | continue; 476 | } 477 | 478 | // phpcs:disable 479 | if (@chmod($fullpath, intval($mode, 8))) { 480 | // phpcs:enable 481 | $this->_messages[] = sprintf('%s changed to %s', $fullpath, $mode); 482 | } else { 483 | $this->_errors[] = sprintf('%s NOT changed to %s', $fullpath, $mode); 484 | } 485 | } 486 | } 487 | 488 | if (empty($this->_errors)) { 489 | return true; 490 | } 491 | } 492 | 493 | return false; 494 | } 495 | 496 | /** 497 | * Returns an array of subdirectories for the provided or current path. 498 | * 499 | * @param string|null $path The directory path to get subdirectories for. 500 | * @param bool $fullPath Whether to return the full path or only the directory name. 501 | * @return array Array of subdirectories for the provided or current path. 502 | */ 503 | public function subdirectories(?string $path = null, bool $fullPath = true): array 504 | { 505 | if (!$path) { 506 | $path = $this->path; 507 | } 508 | $subdirectories = []; 509 | 510 | try { 511 | $iterator = new DirectoryIterator($path); 512 | } catch (Exception $e) { 513 | return []; 514 | } 515 | 516 | foreach ($iterator as $item) { 517 | if (!$item->isDir() || $item->isDot()) { 518 | continue; 519 | } 520 | $subdirectories[] = $fullPath ? $item->getRealPath() : $item->getFilename(); 521 | } 522 | 523 | return $subdirectories; 524 | } 525 | 526 | /** 527 | * Returns an array of nested directories and files in each directory 528 | * 529 | * @param string|null $path the directory path to build the tree from 530 | * @param array|bool $exceptions Either an array of files/folder to exclude 531 | * or boolean true to not grab dot files/folders 532 | * @param string|null $type either 'file' or 'dir'. Null returns both files and directories 533 | * @return array Array of nested directories and files in each directory 534 | */ 535 | public function tree(?string $path = null, $exceptions = false, ?string $type = null): array 536 | { 537 | if (!$path) { 538 | $path = $this->path; 539 | } 540 | $files = []; 541 | $directories = [$path]; 542 | 543 | if (is_array($exceptions)) { 544 | $exceptions = array_flip($exceptions); 545 | } 546 | $skipHidden = false; 547 | if ($exceptions === true) { 548 | $skipHidden = true; 549 | } elseif (isset($exceptions['.'])) { 550 | $skipHidden = true; 551 | unset($exceptions['.']); 552 | } 553 | 554 | try { 555 | $directory = new RecursiveDirectoryIterator( 556 | $path, 557 | RecursiveDirectoryIterator::KEY_AS_PATHNAME | RecursiveDirectoryIterator::CURRENT_AS_SELF 558 | ); 559 | $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST); 560 | } catch (Exception $e) { 561 | unset($directory, $iterator); 562 | 563 | if ($type === null) { 564 | return [[], []]; 565 | } 566 | 567 | return []; 568 | } 569 | 570 | /** 571 | * @var string $itemPath 572 | * @var \RecursiveDirectoryIterator $fsIterator 573 | */ 574 | foreach ($iterator as $itemPath => $fsIterator) { 575 | if ($skipHidden) { 576 | $subPathName = $fsIterator->getSubPathname(); 577 | if ($subPathName[0] === '.' || strpos($subPathName, DIRECTORY_SEPARATOR . '.') !== false) { 578 | unset($fsIterator); 579 | continue; 580 | } 581 | } 582 | /** @var \FilesystemIterator $item */ 583 | $item = $fsIterator->current(); 584 | if (!empty($exceptions) && isset($exceptions[$item->getFilename()])) { 585 | unset($fsIterator, $item); 586 | continue; 587 | } 588 | 589 | if ($item->isFile()) { 590 | $files[] = $itemPath; 591 | } elseif ($item->isDir() && !$item->isDot()) { 592 | $directories[] = $itemPath; 593 | } 594 | 595 | // inner iterators need to be unset too in order for locks on parents to be released 596 | unset($fsIterator, $item); 597 | } 598 | 599 | // unsetting iterators helps releasing possible locks in certain environments, 600 | // which could otherwise make `rmdir()` fail 601 | unset($directory, $iterator); 602 | 603 | if ($type === null) { 604 | return [$directories, $files]; 605 | } 606 | if ($type === 'dir') { 607 | return $directories; 608 | } 609 | 610 | return $files; 611 | } 612 | 613 | /** 614 | * Create a directory structure recursively. 615 | * 616 | * Can be used to create deep path structures like `/foo/bar/baz/shoe/horn` 617 | * 618 | * @param string $pathname The directory structure to create. Either an absolute or relative 619 | * path. If the path is relative and exists in the process' cwd it will not be created. 620 | * Otherwise relative paths will be prefixed with the current pwd(). 621 | * @param int|null $mode octal value 0755 622 | * @return bool Returns TRUE on success, FALSE on failure 623 | */ 624 | public function create(string $pathname, ?int $mode = null): bool 625 | { 626 | if (is_dir($pathname) || empty($pathname)) { 627 | return true; 628 | } 629 | 630 | if (!self::isAbsolute($pathname)) { 631 | $pathname = self::addPathElement($this->pwd(), $pathname); 632 | } 633 | 634 | if (!$mode) { 635 | $mode = $this->mode; 636 | } 637 | 638 | if (is_file($pathname)) { 639 | $this->_errors[] = sprintf('%s is a file', $pathname); 640 | 641 | return false; 642 | } 643 | $pathname = rtrim($pathname, DIRECTORY_SEPARATOR); 644 | $nextPathname = substr($pathname, 0, strrpos($pathname, DIRECTORY_SEPARATOR)); 645 | 646 | if ($this->create($nextPathname, $mode)) { 647 | if (!file_exists($pathname)) { 648 | $old = umask(0); 649 | if (mkdir($pathname, $mode, true)) { 650 | umask($old); 651 | $this->_messages[] = sprintf('%s created', $pathname); 652 | 653 | return true; 654 | } 655 | umask($old); 656 | $this->_errors[] = sprintf('%s NOT created', $pathname); 657 | 658 | return false; 659 | } 660 | } 661 | 662 | return false; 663 | } 664 | 665 | /** 666 | * Returns the size in bytes of this Folder and its contents. 667 | * 668 | * @return int size in bytes of current folder 669 | */ 670 | public function dirsize(): int 671 | { 672 | $size = 0; 673 | $directory = Folder::slashTerm($this->path); 674 | $stack = [$directory]; 675 | $count = count($stack); 676 | for ($i = 0, $j = $count; $i < $j; $i++) { 677 | if (is_file($stack[$i])) { 678 | $size += filesize($stack[$i]); 679 | } elseif (is_dir($stack[$i])) { 680 | $dir = dir($stack[$i]); 681 | if ($dir) { 682 | while (($entry = $dir->read()) !== false) { 683 | if ($entry === '.' || $entry === '..') { 684 | continue; 685 | } 686 | $add = $stack[$i] . $entry; 687 | 688 | if (is_dir($stack[$i] . $entry)) { 689 | $add = Folder::slashTerm($add); 690 | } 691 | $stack[] = $add; 692 | } 693 | $dir->close(); 694 | } 695 | } 696 | $j = count($stack); 697 | } 698 | 699 | return $size; 700 | } 701 | 702 | /** 703 | * Recursively Remove directories if the system allows. 704 | * 705 | * @param string|null $path Path of directory to delete 706 | * @return bool Success 707 | */ 708 | public function delete(?string $path = null): bool 709 | { 710 | if (!$path) { 711 | $path = $this->pwd(); 712 | } 713 | if (!$path) { 714 | return false; 715 | } 716 | $path = Folder::slashTerm($path); 717 | if (is_dir($path)) { 718 | try { 719 | $directory = new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::CURRENT_AS_SELF); 720 | $iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::CHILD_FIRST); 721 | } catch (Exception $e) { 722 | unset($directory, $iterator); 723 | 724 | return false; 725 | } 726 | 727 | foreach ($iterator as $item) { 728 | $filePath = $item->getPathname(); 729 | if ($item->isFile() || $item->isLink()) { 730 | // phpcs:disable 731 | if (@unlink($filePath)) { 732 | // phpcs:enable 733 | $this->_messages[] = sprintf('%s removed', $filePath); 734 | } else { 735 | $this->_errors[] = sprintf('%s NOT removed', $filePath); 736 | } 737 | } elseif ($item->isDir() && !$item->isDot()) { 738 | // phpcs:disable 739 | if (@rmdir($filePath)) { 740 | // phpcs:enable 741 | $this->_messages[] = sprintf('%s removed', $filePath); 742 | } else { 743 | $this->_errors[] = sprintf('%s NOT removed', $filePath); 744 | 745 | unset($directory, $iterator, $item); 746 | 747 | return false; 748 | } 749 | } 750 | 751 | // inner iterators need to be unset too in order for locks on parents to be released 752 | unset($item); 753 | } 754 | 755 | // unsetting iterators helps releasing possible locks in certain environments, 756 | // which could otherwise make `rmdir()` fail 757 | unset($directory, $iterator); 758 | 759 | $path = rtrim($path, DIRECTORY_SEPARATOR); 760 | // phpcs:disable 761 | if (@rmdir($path)) { 762 | // phpcs:enable 763 | $this->_messages[] = sprintf('%s removed', $path); 764 | } else { 765 | $this->_errors[] = sprintf('%s NOT removed', $path); 766 | 767 | return false; 768 | } 769 | } 770 | 771 | return true; 772 | } 773 | 774 | /** 775 | * Recursive directory copy. 776 | * 777 | * ### Options 778 | * 779 | * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). 780 | * - `mode` The mode to copy the files/directories with as integer, e.g. 0775. 781 | * - `skip` Files/directories to skip. 782 | * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP 783 | * - `recursive` Whether to copy recursively or not (default: true - recursive) 784 | * 785 | * @param string $to The directory to copy to. 786 | * @param array $options Array of options (see above). 787 | * @return bool Success. 788 | */ 789 | public function copy(string $to, array $options = []): bool 790 | { 791 | if (!$this->pwd()) { 792 | return false; 793 | } 794 | $options += [ 795 | 'from' => $this->path, 796 | 'mode' => $this->mode, 797 | 'skip' => [], 798 | 'scheme' => Folder::MERGE, 799 | 'recursive' => true, 800 | ]; 801 | 802 | $fromDir = $options['from']; 803 | $toDir = $to; 804 | $mode = $options['mode']; 805 | 806 | if (!$this->cd($fromDir)) { 807 | $this->_errors[] = sprintf('%s not found', $fromDir); 808 | 809 | return false; 810 | } 811 | 812 | if (!is_dir($toDir)) { 813 | $this->create($toDir, $mode); 814 | } 815 | 816 | if (!is_writable($toDir)) { 817 | $this->_errors[] = sprintf('%s not writable', $toDir); 818 | 819 | return false; 820 | } 821 | 822 | $exceptions = array_merge(['.', '..', '.svn'], $options['skip']); 823 | // phpcs:disable 824 | if ($handle = @opendir($fromDir)) { 825 | // phpcs:enable 826 | while (($item = readdir($handle)) !== false) { 827 | $to = Folder::addPathElement($toDir, $item); 828 | if (($options['scheme'] !== Folder::SKIP || !is_dir($to)) && !in_array($item, $exceptions, true)) { 829 | $from = Folder::addPathElement($fromDir, $item); 830 | if (is_file($from) && (!is_file($to) || $options['scheme'] !== Folder::SKIP)) { 831 | if (copy($from, $to)) { 832 | chmod($to, intval($mode, 8)); 833 | touch($to, filemtime($from)); 834 | $this->_messages[] = sprintf('%s copied to %s', $from, $to); 835 | } else { 836 | $this->_errors[] = sprintf('%s NOT copied to %s', $from, $to); 837 | } 838 | } 839 | 840 | if (is_dir($from) && file_exists($to) && $options['scheme'] === Folder::OVERWRITE) { 841 | $this->delete($to); 842 | } 843 | 844 | if (is_dir($from) && $options['recursive'] === false) { 845 | continue; 846 | } 847 | 848 | if (is_dir($from) && !file_exists($to)) { 849 | $old = umask(0); 850 | if (mkdir($to, $mode, true)) { 851 | umask($old); 852 | $old = umask(0); 853 | chmod($to, $mode); 854 | umask($old); 855 | $this->_messages[] = sprintf('%s created', $to); 856 | $options = ['from' => $from] + $options; 857 | $this->copy($to, $options); 858 | } else { 859 | $this->_errors[] = sprintf('%s not created', $to); 860 | } 861 | } elseif (is_dir($from) && $options['scheme'] === Folder::MERGE) { 862 | $options = ['from' => $from] + $options; 863 | $this->copy($to, $options); 864 | } 865 | } 866 | } 867 | closedir($handle); 868 | } else { 869 | return false; 870 | } 871 | 872 | return empty($this->_errors); 873 | } 874 | 875 | /** 876 | * Recursive directory move. 877 | * 878 | * ### Options 879 | * 880 | * - `from` The directory to copy from, this will cause a cd() to occur, changing the results of pwd(). 881 | * - `mode` The mode to copy the files/directories with as integer, e.g. 0775. 882 | * - `skip` Files/directories to skip. 883 | * - `scheme` Folder::MERGE, Folder::OVERWRITE, Folder::SKIP 884 | * - `recursive` Whether to copy recursively or not (default: true - recursive) 885 | * 886 | * @param string $to The directory to move to. 887 | * @param array $options Array of options (see above). 888 | * @return bool Success 889 | */ 890 | public function move(string $to, array $options = []): bool 891 | { 892 | $options += ['from' => $this->path, 'mode' => $this->mode, 'skip' => [], 'recursive' => true]; 893 | 894 | if ($this->copy($to, $options) && $this->delete($options['from'])) { 895 | return (bool)$this->cd($to); 896 | } 897 | 898 | return false; 899 | } 900 | 901 | /** 902 | * get messages from latest method 903 | * 904 | * @param bool $reset Reset message stack after reading 905 | * @return array 906 | */ 907 | public function messages(bool $reset = true): array 908 | { 909 | $messages = $this->_messages; 910 | if ($reset) { 911 | $this->_messages = []; 912 | } 913 | 914 | return $messages; 915 | } 916 | 917 | /** 918 | * get error from latest method 919 | * 920 | * @param bool $reset Reset error stack after reading 921 | * @return array 922 | */ 923 | public function errors(bool $reset = true): array 924 | { 925 | $errors = $this->_errors; 926 | if ($reset) { 927 | $this->_errors = []; 928 | } 929 | 930 | return $errors; 931 | } 932 | 933 | /** 934 | * Get the real path (taking ".." and such into account) 935 | * 936 | * @param string $path Path to resolve 937 | * @return string|false The resolved path 938 | */ 939 | public function realpath($path) 940 | { 941 | if (strpos($path, '..') === false) { 942 | if (!Folder::isAbsolute($path)) { 943 | $path = Folder::addPathElement($this->path, $path); 944 | } 945 | 946 | return $path; 947 | } 948 | $path = str_replace('/', DIRECTORY_SEPARATOR, trim($path)); 949 | $parts = explode(DIRECTORY_SEPARATOR, $path); 950 | $newparts = []; 951 | $newpath = ''; 952 | if ($path[0] === DIRECTORY_SEPARATOR) { 953 | $newpath = DIRECTORY_SEPARATOR; 954 | } 955 | 956 | while (($part = array_shift($parts)) !== null) { 957 | if ($part === '.' || $part === '') { 958 | continue; 959 | } 960 | if ($part === '..') { 961 | if (!empty($newparts)) { 962 | array_pop($newparts); 963 | continue; 964 | } 965 | 966 | return false; 967 | } 968 | $newparts[] = $part; 969 | } 970 | $newpath .= implode(DIRECTORY_SEPARATOR, $newparts); 971 | 972 | return Folder::slashTerm($newpath); 973 | } 974 | 975 | /** 976 | * Returns true if given $path ends in a slash (i.e. is slash-terminated). 977 | * 978 | * @param string $path Path to check 979 | * @return bool true if path ends with slash, false otherwise 980 | */ 981 | public static function isSlashTerm(string $path): bool 982 | { 983 | $lastChar = $path[strlen($path) - 1]; 984 | 985 | return $lastChar === '/' || $lastChar === '\\'; 986 | } 987 | } 988 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | CakePHP(tm) : The Rapid Development PHP Framework (https://cakephp.org) 4 | Copyright (c) 2005-2020, Cake Software Foundation, Inc. (https://cakefoundation.org) 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Total Downloads](https://img.shields.io/packagist/dt/cakephp/filesystem.svg?style=flat-square)](https://packagist.org/packages/cakephp/filesystem) 2 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.txt) 3 | 4 | # This package has been deprecated. 5 | 6 | ## CakePHP Filesystem Library 7 | 8 | The Folder and File utilities are convenience classes to help you read from and write/append to files; list files within a folder and other common directory related tasks. 9 | 10 | ## Basic Usage 11 | 12 | Create a folder instance and search for all the `.php` files within it: 13 | 14 | ```php 15 | use Cake\Filesystem\Folder; 16 | 17 | $dir = new Folder('/path/to/folder'); 18 | $files = $dir->find('.*\.php'); 19 | ``` 20 | 21 | Now you can loop through the files and read from or write/append to the contents or simply delete the file: 22 | 23 | ```php 24 | foreach ($files as $file) { 25 | $file = new File($dir->pwd() . DIRECTORY_SEPARATOR . $file); 26 | $contents = $file->read(); 27 | // $file->write('I am overwriting the contents of this file'); 28 | // $file->append('I am adding to the bottom of this file.'); 29 | // $file->delete(); // I am deleting this file 30 | $file->close(); // Be sure to close the file when you're done 31 | } 32 | ``` 33 | 34 | ## Documentation 35 | 36 | Please make sure you check the [official 37 | documentation](https://book.cakephp.org/4/en/core-libraries/file-folder.html) 38 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cakephp/filesystem", 3 | "description": "CakePHP filesystem convenience classes to help you work with files and folders.", 4 | "type": "library", 5 | "keywords": [ 6 | "cakephp", 7 | "filesystem", 8 | "files", 9 | "folders" 10 | ], 11 | "homepage": "https://cakephp.org", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "CakePHP Community", 16 | "homepage": "https://github.com/cakephp/filesystem/graphs/contributors" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/cakephp/cakephp/issues", 21 | "forum": "https://stackoverflow.com/tags/cakephp", 22 | "irc": "irc://irc.freenode.org/cakephp", 23 | "source": "https://github.com/cakephp/filesystem" 24 | }, 25 | "require": { 26 | "php": ">=7.2.0", 27 | "cakephp/core": "^4.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Cake\\Filesystem\\": "." 32 | } 33 | } 34 | } 35 | --------------------------------------------------------------------------------