This commit is contained in:
Sebastian Molenda
2026-05-12 21:10:38 +02:00
commit ab96d82fcf
2544 changed files with 721700 additions and 0 deletions
+278
View File
@@ -0,0 +1,278 @@
<?php
declare(strict_types=1);
/**
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
* @link https://cakephp.org CakePHP(tm) Project
* @since 4.0.0
* @license https://opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Utility;
use Cake\Core\Exception\CakeException;
use CallbackFilterIterator;
use Closure;
use FilesystemIterator;
use Iterator;
use RecursiveCallbackFilterIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use SplFileInfo;
/**
* This provides convenience wrappers around common filesystem queries.
*
* This is an internal helper class that should not be used in application code
* as it provides no guarantee for compatibility.
*
* @internal
*/
class Filesystem
{
/**
* Directory type constant
*
* @var string
*/
public const TYPE_DIR = 'dir';
/**
* Find files / directories (non-recursively) in given directory path.
*
* @param string $path Directory path.
* @param \Closure|string|null $filter If string will be used as regex for filtering using
* `RegexIterator`, if callable will be as callback for `CallbackFilterIterator`.
* @param int|null $flags Flags for FilesystemIterator::__construct();
* @return \Iterator
*/
public function find(string $path, Closure|string|null $filter = null, ?int $flags = null): Iterator
{
$flags ??= FilesystemIterator::KEY_AS_PATHNAME
| FilesystemIterator::CURRENT_AS_FILEINFO
| FilesystemIterator::SKIP_DOTS;
$directory = new FilesystemIterator($path, $flags);
if ($filter === null) {
return $directory;
}
return $this->filterIterator($directory, $filter);
}
/**
* Find files/ directories recursively in given directory path.
*
* @param string $path Directory path.
* @param \Closure|string|null $filter If string will be used as regex for filtering using
* `RegexIterator`, if callable will be as callback for `CallbackFilterIterator`.
* Hidden directories (starting with dot e.g. .git) are always skipped.
* @param int|null $flags Flags for FilesystemIterator::__construct();
* @return \Iterator
*/
public function findRecursive(string $path, Closure|string|null $filter = null, ?int $flags = null): Iterator
{
$flags ??= FilesystemIterator::KEY_AS_PATHNAME
| FilesystemIterator::CURRENT_AS_FILEINFO
| FilesystemIterator::SKIP_DOTS;
$directory = new RecursiveDirectoryIterator($path, $flags);
$dirFilter = new RecursiveCallbackFilterIterator(
$directory,
function (SplFileInfo $current) {
if (str_starts_with($current->getFilename(), '.') && $current->isDir()) {
return false;
}
return true;
},
);
$flatten = new RecursiveIteratorIterator(
$dirFilter,
RecursiveIteratorIterator::CHILD_FIRST,
);
if ($filter === null) {
return $flatten;
}
return $this->filterIterator($flatten, $filter);
}
/**
* Wrap iterator in additional filtering iterator.
*
* @param \Iterator $iterator Iterator
* @param \Closure|string $filter Regex string or callback.
* @return \Iterator
*/
protected function filterIterator(Iterator $iterator, Closure|string $filter): Iterator
{
if (is_string($filter)) {
return new RegexIterator($iterator, $filter);
}
return new CallbackFilterIterator($iterator, $filter);
}
/**
* Dump contents to file.
*
* @param string $filename File path.
* @param string $content Content to dump.
* @return void
* @throws \Cake\Core\Exception\CakeException When dumping fails.
*/
public function dumpFile(string $filename, string $content): void
{
$dir = dirname($filename);
if (!is_dir($dir)) {
$this->mkdir($dir);
}
$exists = file_exists($filename);
if ($this->isStream($filename)) {
// phpcs:ignore
$success = @file_put_contents($filename, $content);
} else {
// phpcs:ignore
$success = @file_put_contents($filename, $content, LOCK_EX);
}
if ($success === false) {
throw new CakeException(sprintf('Failed dumping content to file `%s`', $dir));
}
if (!$exists) {
chmod($filename, 0666 & ~umask());
}
}
/**
* Create directory.
*
* @param string $dir Directory path.
* @param int $mode Octal mode passed to mkdir(). Defaults to 0777.
* @return void
* @throws \Cake\Core\Exception\CakeException When directory creation fails.
*/
public function mkdir(string $dir, int $mode = 0777): void
{
if (is_dir($dir)) {
return;
}
$old = umask(0);
// phpcs:ignore
if (@mkdir($dir, $mode, true) === false) {
umask($old);
throw new CakeException(sprintf('Failed to create directory `%s`', $dir));
}
umask($old);
}
/**
* Delete directory along with all it's contents.
*
* @param string $path Directory path.
* @return bool
* @throws \Cake\Core\Exception\CakeException If path is not a directory.
*/
public function deleteDir(string $path): bool
{
if (!file_exists($path)) {
return true;
}
if (!is_dir($path)) {
throw new CakeException(sprintf('`%s` is not a directory', $path));
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST,
);
$result = true;
/** @var \SplFileInfo $fileInfo */
foreach ($iterator as $fileInfo) {
$isWindowsLink = DIRECTORY_SEPARATOR === '\\' && $fileInfo->getType() === 'link';
if ($fileInfo->getType() === self::TYPE_DIR || $isWindowsLink) {
// phpcs:ignore
$result = $result && @rmdir($fileInfo->getPathname());
unset($fileInfo);
continue;
}
// phpcs:ignore
$result = $result && @unlink($fileInfo->getPathname());
// possible inner iterators need to be unset too in order for locks on parents to be released
unset($fileInfo);
}
// unsetting iterators helps releasing possible locks in certain environments,
// which could otherwise make `rmdir()` fail
unset($iterator);
// phpcs:ignore
return $result && @rmdir($path);
}
/**
* Copies directory with all it's contents.
*
* @param string $source Source path.
* @param string $destination Destination path.
* @return bool
*/
public function copyDir(string $source, string $destination): bool
{
$destination = (new SplFileInfo($destination))->getPathname();
if (!is_dir($destination)) {
$this->mkdir($destination);
}
/** @var \FilesystemIterator<\SplFileInfo> $iterator */
$iterator = new FilesystemIterator($source);
$result = true;
foreach ($iterator as $fileInfo) {
if ($fileInfo->isDir()) {
$result = $result && $this->copyDir(
$fileInfo->getPathname(),
$destination . DIRECTORY_SEPARATOR . $fileInfo->getFilename(),
);
} else {
// phpcs:ignore
$result = $result && @copy(
$fileInfo->getPathname(),
$destination . DIRECTORY_SEPARATOR . $fileInfo->getFilename(),
);
}
}
return $result;
}
/**
* Check whether the given path is a stream path.
*
* @param string $path Path.
* @return bool
*/
public function isStream(string $path): bool
{
return str_contains($path, '://');
}
}