Added Symfony/Filesystem
This commit is contained in:
parent
86a2132778
commit
7f2e73c045
15 changed files with 1833 additions and 139 deletions
|
@ -20,6 +20,7 @@
|
||||||
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'polyfill-mbstring' . DIRECTORY_SEPARATOR . 'bootstrap.php',
|
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'polyfill-mbstring' . DIRECTORY_SEPARATOR . 'bootstrap.php',
|
||||||
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Process' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
|
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Process' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
|
||||||
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Uid' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
|
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Uid' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
|
||||||
|
$third_party_path . 'Symfony' . DIRECTORY_SEPARATOR . 'Filesystem' . DIRECTORY_SEPARATOR . 'autoload_spl.php',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach($target_files as $file)
|
foreach($target_files as $file)
|
||||||
|
|
|
@ -1,139 +0,0 @@
|
||||||
#!/bin/php
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
# Nosial Code Compiler (NCC) Installation Script
|
|
||||||
#
|
|
||||||
# Nosial Code Compiler is a program written in PHP designed
|
|
||||||
# to be a multi-purpose compiler, package manager and toolkit.
|
|
||||||
#
|
|
||||||
# Dependency:
|
|
||||||
# PHP 8.0+
|
|
||||||
# ------------------------------------------------------------------
|
|
||||||
|
|
||||||
<?PHP
|
|
||||||
// TODO: Check for root before proceeding
|
|
||||||
|
|
||||||
# Global Variables
|
|
||||||
$NCC_INSTALL_PATH='/etc/ncc';
|
|
||||||
$NCC_DATA_PATH='/var/ncc';
|
|
||||||
$NCC_UPDATE_SOURCE='https://updates.nosial.com/ncc/check_updates?current_version=$VERSION'; # Unused
|
|
||||||
$NCC_CHECKSUM=__DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin';
|
|
||||||
$NCC_AUTOLOAD=__DIR__ . DIRECTORY_SEPARATOR . 'autoload.php';
|
|
||||||
|
|
||||||
// Require NCC
|
|
||||||
if(!file_exists($NCC_AUTOLOAD))
|
|
||||||
{
|
|
||||||
print('The file \'autoload.php\' was not found, installation cannot proceed.' . PHP_EOL);
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
require($NCC_AUTOLOAD);
|
|
||||||
|
|
||||||
// Check for the required files
|
|
||||||
$required_files = [
|
|
||||||
'LICENSE',
|
|
||||||
'build_files'
|
|
||||||
];
|
|
||||||
foreach($required_files as $file)
|
|
||||||
{
|
|
||||||
if(!file_exists($file))
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outError('Missing file \'' . $file . '\', installation failed.', true, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preform the checksum validation
|
|
||||||
if(!file_exists($NCC_CHECKSUM))
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outWarning('The file \'checksum.bin\' was not found, the contents of the program cannot be verified to be safe');
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::out('Running checksum');
|
|
||||||
|
|
||||||
$checksum = \ncc\ZiProto\ZiProto::decode(file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'checksum.bin'));
|
|
||||||
$checksum_failed = false;
|
|
||||||
|
|
||||||
foreach($checksum as $file => $hash)
|
|
||||||
{
|
|
||||||
if(file_exists(__DIR__ . DIRECTORY_SEPARATOR . $file) == false)
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outError('Cannot check file, \'' . $file . '\' not found.');
|
|
||||||
$checksum_failed = true;
|
|
||||||
}
|
|
||||||
elseif(hash_file('sha256', __DIR__ . DIRECTORY_SEPARATOR . $file) !== $hash)
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outWarning('The file \'' . $file . '\' does not match the original checksum');
|
|
||||||
$checksum_failed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if($checksum_failed)
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outError('Checksum failed, the contents of the program cannot be verified to be safe');
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::out('Checksum passed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for required extensions
|
|
||||||
foreach(\ncc\Utilities\Validate::requiredExtensions() as $ext => $installed)
|
|
||||||
{
|
|
||||||
if(!$installed)
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outWarning('The extension \'' . $ext . '\' is not installed, compatibility without it is not guaranteed');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start of installer
|
|
||||||
\ncc\Utilities\Console::out('Started NCC installer');
|
|
||||||
|
|
||||||
// Determine the installation path
|
|
||||||
// TODO: Add the ability to change the data path as well
|
|
||||||
while(true)
|
|
||||||
{
|
|
||||||
$user_input = null;
|
|
||||||
$user_input = \ncc\Utilities\Console::getInput("Installation Path (Default: $NCC_INSTALL_PATH): ");
|
|
||||||
if(strlen($user_input) > 0)
|
|
||||||
{
|
|
||||||
if(file_exists($user_input))
|
|
||||||
{
|
|
||||||
if(file_exists($user_input . DIRECTORY_SEPARATOR . 'ncc'))
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::out('NCC Seems to already be installed, the installer will repair/upgrade your current install');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
\ncc\Utilities\Console::outError('The given directory already exists, it must be deleted before proceeding');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
\ncc\Utilities\Console::out("Note: This doesn't affect your current install of composer (if you have composer installed)");
|
|
||||||
$update_composer = \ncc\Utilities\Console::getBooleanInput('Do you want to install composer for NCC? (Recommended)');
|
|
||||||
|
|
||||||
// Prepare installation
|
|
||||||
if(file_exists($NCC_INSTALL_PATH))
|
|
||||||
{
|
|
||||||
// TODO: Implement recursive directory removal
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdir($NCC_INSTALL_PATH);
|
|
||||||
mkdir($NCC_DATA_PATH);
|
|
||||||
|
|
||||||
// Install composer
|
|
||||||
|
|
||||||
// Install NCC
|
|
||||||
?>
|
|
82
src/ncc/ThirdParty/Symfony/Filesystem/CHANGELOG.md
vendored
Normal file
82
src/ncc/ThirdParty/Symfony/Filesystem/CHANGELOG.md
vendored
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
CHANGELOG
|
||||||
|
=========
|
||||||
|
|
||||||
|
5.4
|
||||||
|
---
|
||||||
|
|
||||||
|
* Add `Path` class
|
||||||
|
* Add `$lock` argument to `Filesystem::appendToFile()`
|
||||||
|
|
||||||
|
5.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* `Filesystem::dumpFile()` and `appendToFile()` don't accept arrays anymore
|
||||||
|
|
||||||
|
4.4.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* support for passing a `null` value to `Filesystem::isAbsolutePath()` is deprecated and will be removed in 5.0
|
||||||
|
* `tempnam()` now accepts a third argument `$suffix`.
|
||||||
|
|
||||||
|
4.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* support for passing arrays to `Filesystem::dumpFile()` is deprecated and will be removed in 5.0
|
||||||
|
* support for passing arrays to `Filesystem::appendToFile()` is deprecated and will be removed in 5.0
|
||||||
|
|
||||||
|
4.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* removed `LockHandler`
|
||||||
|
* Support for passing relative paths to `Filesystem::makePathRelative()` has been removed.
|
||||||
|
|
||||||
|
3.4.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* support for passing relative paths to `Filesystem::makePathRelative()` is deprecated and will be removed in 4.0
|
||||||
|
|
||||||
|
3.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added `appendToFile()` to append contents to existing files
|
||||||
|
|
||||||
|
3.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added `readlink()` as a platform independent method to read links
|
||||||
|
|
||||||
|
3.0.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* removed `$mode` argument from `Filesystem::dumpFile()`
|
||||||
|
|
||||||
|
2.8.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added tempnam() a stream aware version of PHP's native tempnam()
|
||||||
|
|
||||||
|
2.6.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added LockHandler
|
||||||
|
|
||||||
|
2.3.12
|
||||||
|
------
|
||||||
|
|
||||||
|
* deprecated dumpFile() file mode argument.
|
||||||
|
|
||||||
|
2.3.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added the dumpFile() method to atomically write files
|
||||||
|
|
||||||
|
2.2.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* added a delete option for the mirror() method
|
||||||
|
|
||||||
|
2.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* 24eb396 : BC Break : mkdir() function now throws exception in case of failure instead of returning Boolean value
|
||||||
|
* created the component
|
21
src/ncc/ThirdParty/Symfony/Filesystem/Exception/ExceptionInterface.php
vendored
Normal file
21
src/ncc/ThirdParty/Symfony/Filesystem/Exception/ExceptionInterface.php
vendored
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception interface for all exceptions thrown by the component.
|
||||||
|
*
|
||||||
|
* @author Romain Neutron <imprec@gmail.com>
|
||||||
|
*/
|
||||||
|
interface ExceptionInterface extends \Throwable
|
||||||
|
{
|
||||||
|
}
|
34
src/ncc/ThirdParty/Symfony/Filesystem/Exception/FileNotFoundException.php
vendored
Normal file
34
src/ncc/ThirdParty/Symfony/Filesystem/Exception/FileNotFoundException.php
vendored
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception class thrown when a file couldn't be found.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
|
||||||
|
*/
|
||||||
|
class FileNotFoundException extends IOException
|
||||||
|
{
|
||||||
|
public function __construct(string $message = null, int $code = 0, \Throwable $previous = null, string $path = null)
|
||||||
|
{
|
||||||
|
if (null === $message) {
|
||||||
|
if (null === $path) {
|
||||||
|
$message = 'File could not be found.';
|
||||||
|
} else {
|
||||||
|
$message = sprintf('File "%s" could not be found.', $path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct($message, $code, $previous, $path);
|
||||||
|
}
|
||||||
|
}
|
39
src/ncc/ThirdParty/Symfony/Filesystem/Exception/IOException.php
vendored
Normal file
39
src/ncc/ThirdParty/Symfony/Filesystem/Exception/IOException.php
vendored
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception class thrown when a filesystem operation failure happens.
|
||||||
|
*
|
||||||
|
* @author Romain Neutron <imprec@gmail.com>
|
||||||
|
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*/
|
||||||
|
class IOException extends \RuntimeException implements IOExceptionInterface
|
||||||
|
{
|
||||||
|
private ?string $path;
|
||||||
|
|
||||||
|
public function __construct(string $message, int $code = 0, \Throwable $previous = null, string $path = null)
|
||||||
|
{
|
||||||
|
$this->path = $path;
|
||||||
|
|
||||||
|
parent::__construct($message, $code, $previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getPath(): ?string
|
||||||
|
{
|
||||||
|
return $this->path;
|
||||||
|
}
|
||||||
|
}
|
25
src/ncc/ThirdParty/Symfony/Filesystem/Exception/IOExceptionInterface.php
vendored
Normal file
25
src/ncc/ThirdParty/Symfony/Filesystem/Exception/IOExceptionInterface.php
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IOException interface for file and input/output stream related exceptions thrown by the component.
|
||||||
|
*
|
||||||
|
* @author Christian Gärtner <christiangaertner.film@googlemail.com>
|
||||||
|
*/
|
||||||
|
interface IOExceptionInterface extends ExceptionInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Returns the associated path for the exception.
|
||||||
|
*/
|
||||||
|
public function getPath(): ?string;
|
||||||
|
}
|
19
src/ncc/ThirdParty/Symfony/Filesystem/Exception/InvalidArgumentException.php
vendored
Normal file
19
src/ncc/ThirdParty/Symfony/Filesystem/Exception/InvalidArgumentException.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Christian Flothmann <christian.flothmann@sensiolabs.de>
|
||||||
|
*/
|
||||||
|
class InvalidArgumentException extends \InvalidArgumentException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
19
src/ncc/ThirdParty/Symfony/Filesystem/Exception/RuntimeException.php
vendored
Normal file
19
src/ncc/ThirdParty/Symfony/Filesystem/Exception/RuntimeException.php
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem\Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Théo Fidry <theo.fidry@gmail.com>
|
||||||
|
*/
|
||||||
|
class RuntimeException extends \RuntimeException implements ExceptionInterface
|
||||||
|
{
|
||||||
|
}
|
737
src/ncc/ThirdParty/Symfony/Filesystem/Filesystem.php
vendored
Normal file
737
src/ncc/ThirdParty/Symfony/Filesystem/Filesystem.php
vendored
Normal file
|
@ -0,0 +1,737 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem;
|
||||||
|
|
||||||
|
use ncc\ThirdParty\Symfony\Filesystem\Exception\FileNotFoundException;
|
||||||
|
use ncc\ThirdParty\Symfony\Filesystem\Exception\InvalidArgumentException;
|
||||||
|
use ncc\ThirdParty\Symfony\Filesystem\Exception\IOException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides basic utility to manipulate the file system.
|
||||||
|
*
|
||||||
|
* @author Fabien Potencier <fabien@symfony.com>
|
||||||
|
*/
|
||||||
|
class Filesystem
|
||||||
|
{
|
||||||
|
private static $lastError;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copies a file.
|
||||||
|
*
|
||||||
|
* If the target file is older than the origin file, it's always overwritten.
|
||||||
|
* If the target file is newer, it is overwritten only when the
|
||||||
|
* $overwriteNewerFiles option is set to true.
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException When originFile doesn't exist
|
||||||
|
* @throws IOException When copy fails
|
||||||
|
*/
|
||||||
|
public function copy(string $originFile, string $targetFile, bool $overwriteNewerFiles = false)
|
||||||
|
{
|
||||||
|
$originIsLocal = stream_is_local($originFile) || 0 === stripos($originFile, 'file://');
|
||||||
|
if ($originIsLocal && !is_file($originFile)) {
|
||||||
|
throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mkdir(\dirname($targetFile));
|
||||||
|
|
||||||
|
$doCopy = true;
|
||||||
|
if (!$overwriteNewerFiles && null === parse_url($originFile, \PHP_URL_HOST) && is_file($targetFile)) {
|
||||||
|
$doCopy = filemtime($originFile) > filemtime($targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($doCopy) {
|
||||||
|
// https://bugs.php.net/64634
|
||||||
|
if (!$source = self::box('fopen', $originFile, 'r')) {
|
||||||
|
throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
|
||||||
|
if (!$target = self::box('fopen', $targetFile, 'w', false, stream_context_create(['ftp' => ['overwrite' => true]]))) {
|
||||||
|
throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing: ', $originFile, $targetFile).self::$lastError, 0, null, $originFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$bytesCopied = stream_copy_to_stream($source, $target);
|
||||||
|
fclose($source);
|
||||||
|
fclose($target);
|
||||||
|
unset($source, $target);
|
||||||
|
|
||||||
|
if (!is_file($targetFile)) {
|
||||||
|
throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($originIsLocal) {
|
||||||
|
// Like `cp`, preserve executable permission bits
|
||||||
|
self::box('chmod', $targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111));
|
||||||
|
|
||||||
|
if ($bytesCopied !== $bytesOrigin = filesize($originFile)) {
|
||||||
|
throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a directory recursively.
|
||||||
|
*
|
||||||
|
* @throws IOException On any directory creation failure
|
||||||
|
*/
|
||||||
|
public function mkdir(string|iterable $dirs, int $mode = 0777)
|
||||||
|
{
|
||||||
|
foreach ($this->toIterable($dirs) as $dir) {
|
||||||
|
if (is_dir($dir)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::box('mkdir', $dir, $mode, true) && !is_dir($dir)) {
|
||||||
|
throw new IOException(sprintf('Failed to create "%s": ', $dir).self::$lastError, 0, null, $dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks the existence of files or directories.
|
||||||
|
*/
|
||||||
|
public function exists(string|iterable $files): bool
|
||||||
|
{
|
||||||
|
$maxPathLength = \PHP_MAXPATHLEN - 2;
|
||||||
|
|
||||||
|
foreach ($this->toIterable($files) as $file) {
|
||||||
|
if (\strlen($file) > $maxPathLength) {
|
||||||
|
throw new IOException(sprintf('Could not check if file exist because path length exceeds %d characters.', $maxPathLength), 0, null, $file);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($file)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets access and modification time of file.
|
||||||
|
*
|
||||||
|
* @param int|null $time The touch time as a Unix timestamp, if not supplied the current system time is used
|
||||||
|
* @param int|null $atime The access time as a Unix timestamp, if not supplied the current system time is used
|
||||||
|
*
|
||||||
|
* @throws IOException When touch fails
|
||||||
|
*/
|
||||||
|
public function touch(string|iterable $files, int $time = null, int $atime = null)
|
||||||
|
{
|
||||||
|
foreach ($this->toIterable($files) as $file) {
|
||||||
|
if (!($time ? self::box('touch', $file, $time, $atime) : self::box('touch', $file))) {
|
||||||
|
throw new IOException(sprintf('Failed to touch "%s": ', $file).self::$lastError, 0, null, $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes files or directories.
|
||||||
|
*
|
||||||
|
* @throws IOException When removal fails
|
||||||
|
*/
|
||||||
|
public function remove(string|iterable $files)
|
||||||
|
{
|
||||||
|
if ($files instanceof \Traversable) {
|
||||||
|
$files = iterator_to_array($files, false);
|
||||||
|
} elseif (!\is_array($files)) {
|
||||||
|
$files = [$files];
|
||||||
|
}
|
||||||
|
|
||||||
|
self::doRemove($files, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function doRemove(array $files, bool $isRecursive): void
|
||||||
|
{
|
||||||
|
$files = array_reverse($files);
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (is_link($file)) {
|
||||||
|
// See https://bugs.php.net/52176
|
||||||
|
if (!(self::box('unlink', $file) || '\\' !== \DIRECTORY_SEPARATOR || self::box('rmdir', $file)) && file_exists($file)) {
|
||||||
|
throw new IOException(sprintf('Failed to remove symlink "%s": ', $file).self::$lastError);
|
||||||
|
}
|
||||||
|
} elseif (is_dir($file)) {
|
||||||
|
if (!$isRecursive) {
|
||||||
|
$tmpName = \dirname(realpath($file)).'/.'.strrev(strtr(base64_encode(random_bytes(2)), '/=', '-.'));
|
||||||
|
|
||||||
|
if (file_exists($tmpName)) {
|
||||||
|
try {
|
||||||
|
self::doRemove([$tmpName], true);
|
||||||
|
} catch (IOException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file_exists($tmpName) && self::box('rename', $file, $tmpName)) {
|
||||||
|
$origFile = $file;
|
||||||
|
$file = $tmpName;
|
||||||
|
} else {
|
||||||
|
$origFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$filesystemIterator = new \FilesystemIterator($file, \FilesystemIterator::CURRENT_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS);
|
||||||
|
self::doRemove(iterator_to_array($filesystemIterator, true), true);
|
||||||
|
|
||||||
|
if (!self::box('rmdir', $file) && file_exists($file) && !$isRecursive) {
|
||||||
|
$lastError = self::$lastError;
|
||||||
|
|
||||||
|
if (null !== $origFile && self::box('rename', $file, $origFile)) {
|
||||||
|
$file = $origFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException(sprintf('Failed to remove directory "%s": ', $file).$lastError);
|
||||||
|
}
|
||||||
|
} elseif (!self::box('unlink', $file) && (str_contains(self::$lastError, 'Permission denied') || file_exists($file))) {
|
||||||
|
throw new IOException(sprintf('Failed to remove file "%s": ', $file).self::$lastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change mode for an array of files or directories.
|
||||||
|
*
|
||||||
|
* @param int $mode The new mode (octal)
|
||||||
|
* @param int $umask The mode mask (octal)
|
||||||
|
* @param bool $recursive Whether change the mod recursively or not
|
||||||
|
*
|
||||||
|
* @throws IOException When the change fails
|
||||||
|
*/
|
||||||
|
public function chmod(string|iterable $files, int $mode, int $umask = 0000, bool $recursive = false)
|
||||||
|
{
|
||||||
|
foreach ($this->toIterable($files) as $file) {
|
||||||
|
if (\is_int($mode) && !self::box('chmod', $file, $mode & ~$umask)) {
|
||||||
|
throw new IOException(sprintf('Failed to chmod file "%s": ', $file).self::$lastError, 0, null, $file);
|
||||||
|
}
|
||||||
|
if ($recursive && is_dir($file) && !is_link($file)) {
|
||||||
|
$this->chmod(new \FilesystemIterator($file), $mode, $umask, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the owner of an array of files or directories.
|
||||||
|
*
|
||||||
|
* @param string|int $user A user name or number
|
||||||
|
* @param bool $recursive Whether change the owner recursively or not
|
||||||
|
*
|
||||||
|
* @throws IOException When the change fails
|
||||||
|
*/
|
||||||
|
public function chown(string|iterable $files, string|int $user, bool $recursive = false)
|
||||||
|
{
|
||||||
|
foreach ($this->toIterable($files) as $file) {
|
||||||
|
if ($recursive && is_dir($file) && !is_link($file)) {
|
||||||
|
$this->chown(new \FilesystemIterator($file), $user, true);
|
||||||
|
}
|
||||||
|
if (is_link($file) && \function_exists('lchown')) {
|
||||||
|
if (!self::box('lchown', $file, $user)) {
|
||||||
|
throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!self::box('chown', $file, $user)) {
|
||||||
|
throw new IOException(sprintf('Failed to chown file "%s": ', $file).self::$lastError, 0, null, $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the group of an array of files or directories.
|
||||||
|
*
|
||||||
|
* @param string|int $group A group name or number
|
||||||
|
* @param bool $recursive Whether change the group recursively or not
|
||||||
|
*
|
||||||
|
* @throws IOException When the change fails
|
||||||
|
*/
|
||||||
|
public function chgrp(string|iterable $files, string|int $group, bool $recursive = false)
|
||||||
|
{
|
||||||
|
foreach ($this->toIterable($files) as $file) {
|
||||||
|
if ($recursive && is_dir($file) && !is_link($file)) {
|
||||||
|
$this->chgrp(new \FilesystemIterator($file), $group, true);
|
||||||
|
}
|
||||||
|
if (is_link($file) && \function_exists('lchgrp')) {
|
||||||
|
if (!self::box('lchgrp', $file, $group)) {
|
||||||
|
throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!self::box('chgrp', $file, $group)) {
|
||||||
|
throw new IOException(sprintf('Failed to chgrp file "%s": ', $file).self::$lastError, 0, null, $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a file or a directory.
|
||||||
|
*
|
||||||
|
* @throws IOException When target file or directory already exists
|
||||||
|
* @throws IOException When origin cannot be renamed
|
||||||
|
*/
|
||||||
|
public function rename(string $origin, string $target, bool $overwrite = false)
|
||||||
|
{
|
||||||
|
// we check that target does not exist
|
||||||
|
if (!$overwrite && $this->isReadable($target)) {
|
||||||
|
throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::box('rename', $origin, $target)) {
|
||||||
|
if (is_dir($origin)) {
|
||||||
|
// See https://bugs.php.net/54097 & https://php.net/rename#113943
|
||||||
|
$this->mirror($origin, $target, null, ['override' => $overwrite, 'delete' => $overwrite]);
|
||||||
|
$this->remove($origin);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new IOException(sprintf('Cannot rename "%s" to "%s": ', $origin, $target).self::$lastError, 0, null, $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tells whether a file exists and is readable.
|
||||||
|
*
|
||||||
|
* @throws IOException When windows path is longer than 258 characters
|
||||||
|
*/
|
||||||
|
private function isReadable(string $filename): bool
|
||||||
|
{
|
||||||
|
$maxPathLength = \PHP_MAXPATHLEN - 2;
|
||||||
|
|
||||||
|
if (\strlen($filename) > $maxPathLength) {
|
||||||
|
throw new IOException(sprintf('Could not check if file is readable because path length exceeds %d characters.', $maxPathLength), 0, null, $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_readable($filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a symbolic link or copy a directory.
|
||||||
|
*
|
||||||
|
* @throws IOException When symlink fails
|
||||||
|
*/
|
||||||
|
public function symlink(string $originDir, string $targetDir, bool $copyOnWindows = false)
|
||||||
|
{
|
||||||
|
self::assertFunctionExists('symlink');
|
||||||
|
|
||||||
|
if ('\\' === \DIRECTORY_SEPARATOR) {
|
||||||
|
$originDir = strtr($originDir, '/', '\\');
|
||||||
|
$targetDir = strtr($targetDir, '/', '\\');
|
||||||
|
|
||||||
|
if ($copyOnWindows) {
|
||||||
|
$this->mirror($originDir, $targetDir);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mkdir(\dirname($targetDir));
|
||||||
|
|
||||||
|
if (is_link($targetDir)) {
|
||||||
|
if (readlink($targetDir) === $originDir) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->remove($targetDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::box('symlink', $originDir, $targetDir)) {
|
||||||
|
$this->linkException($originDir, $targetDir, 'symbolic');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a hard link, or several hard links to a file.
|
||||||
|
*
|
||||||
|
* @param string|string[] $targetFiles The target file(s)
|
||||||
|
*
|
||||||
|
* @throws FileNotFoundException When original file is missing or not a file
|
||||||
|
* @throws IOException When link fails, including if link already exists
|
||||||
|
*/
|
||||||
|
public function hardlink(string $originFile, string|iterable $targetFiles)
|
||||||
|
{
|
||||||
|
self::assertFunctionExists('link');
|
||||||
|
|
||||||
|
if (!$this->exists($originFile)) {
|
||||||
|
throw new FileNotFoundException(null, 0, null, $originFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_file($originFile)) {
|
||||||
|
throw new FileNotFoundException(sprintf('Origin file "%s" is not a file.', $originFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($this->toIterable($targetFiles) as $targetFile) {
|
||||||
|
if (is_file($targetFile)) {
|
||||||
|
if (fileinode($originFile) === fileinode($targetFile)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$this->remove($targetFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::box('link', $originFile, $targetFile)) {
|
||||||
|
$this->linkException($originFile, $targetFile, 'hard');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
|
||||||
|
*/
|
||||||
|
private function linkException(string $origin, string $target, string $linkType)
|
||||||
|
{
|
||||||
|
if (self::$lastError) {
|
||||||
|
if ('\\' === \DIRECTORY_SEPARATOR && str_contains(self::$lastError, 'error code(1314)')) {
|
||||||
|
throw new IOException(sprintf('Unable to create "%s" link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new IOException(sprintf('Failed to create "%s" link from "%s" to "%s": ', $linkType, $origin, $target).self::$lastError, 0, null, $target);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves links in paths.
|
||||||
|
*
|
||||||
|
* With $canonicalize = false (default)
|
||||||
|
* - if $path does not exist or is not a link, returns null
|
||||||
|
* - if $path is a link, returns the next direct target of the link without considering the existence of the target
|
||||||
|
*
|
||||||
|
* With $canonicalize = true
|
||||||
|
* - if $path does not exist, returns null
|
||||||
|
* - if $path exists, returns its absolute fully resolved final version
|
||||||
|
*/
|
||||||
|
public function readlink(string $path, bool $canonicalize = false): ?string
|
||||||
|
{
|
||||||
|
if (!$canonicalize && !is_link($path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($canonicalize) {
|
||||||
|
if (!$this->exists($path)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return realpath($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return readlink($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an existing path, convert it to a path relative to a given starting path.
|
||||||
|
*/
|
||||||
|
public function makePathRelative(string $endPath, string $startPath): string
|
||||||
|
{
|
||||||
|
if (!$this->isAbsolutePath($startPath)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The start path "%s" is not absolute.', $startPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isAbsolutePath($endPath)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The end path "%s" is not absolute.', $endPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize separators on Windows
|
||||||
|
if ('\\' === \DIRECTORY_SEPARATOR) {
|
||||||
|
$endPath = str_replace('\\', '/', $endPath);
|
||||||
|
$startPath = str_replace('\\', '/', $startPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
$splitDriveLetter = function ($path) {
|
||||||
|
return (\strlen($path) > 2 && ':' === $path[1] && '/' === $path[2] && ctype_alpha($path[0]))
|
||||||
|
? [substr($path, 2), strtoupper($path[0])]
|
||||||
|
: [$path, null];
|
||||||
|
};
|
||||||
|
|
||||||
|
$splitPath = function ($path) {
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach (explode('/', trim($path, '/')) as $segment) {
|
||||||
|
if ('..' === $segment) {
|
||||||
|
array_pop($result);
|
||||||
|
} elseif ('.' !== $segment && '' !== $segment) {
|
||||||
|
$result[] = $segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
};
|
||||||
|
|
||||||
|
[$endPath, $endDriveLetter] = $splitDriveLetter($endPath);
|
||||||
|
[$startPath, $startDriveLetter] = $splitDriveLetter($startPath);
|
||||||
|
|
||||||
|
$startPathArr = $splitPath($startPath);
|
||||||
|
$endPathArr = $splitPath($endPath);
|
||||||
|
|
||||||
|
if ($endDriveLetter && $startDriveLetter && $endDriveLetter != $startDriveLetter) {
|
||||||
|
// End path is on another drive, so no relative path exists
|
||||||
|
return $endDriveLetter.':/'.($endPathArr ? implode('/', $endPathArr).'/' : '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find for which directory the common path stops
|
||||||
|
$index = 0;
|
||||||
|
while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
|
||||||
|
++$index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
|
||||||
|
if (1 === \count($startPathArr) && '' === $startPathArr[0]) {
|
||||||
|
$depth = 0;
|
||||||
|
} else {
|
||||||
|
$depth = \count($startPathArr) - $index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeated "../" for each level need to reach the common path
|
||||||
|
$traverser = str_repeat('../', $depth);
|
||||||
|
|
||||||
|
$endPathRemainder = implode('/', \array_slice($endPathArr, $index));
|
||||||
|
|
||||||
|
// Construct $endPath from traversing to the common path, then to the remaining $endPath
|
||||||
|
$relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : '');
|
||||||
|
|
||||||
|
return '' === $relativePath ? './' : $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mirrors a directory to another.
|
||||||
|
*
|
||||||
|
* Copies files and directories from the origin directory into the target directory. By default:
|
||||||
|
*
|
||||||
|
* - existing files in the target directory will be overwritten, except if they are newer (see the `override` option)
|
||||||
|
* - files in the target directory that do not exist in the source directory will not be deleted (see the `delete` option)
|
||||||
|
*
|
||||||
|
* @param \Traversable|null $iterator Iterator that filters which files and directories to copy, if null a recursive iterator is created
|
||||||
|
* @param array $options An array of boolean options
|
||||||
|
* Valid options are:
|
||||||
|
* - $options['override'] If true, target files newer than origin files are overwritten (see copy(), defaults to false)
|
||||||
|
* - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink(), defaults to false)
|
||||||
|
* - $options['delete'] Whether to delete files that are not in the source directory (defaults to false)
|
||||||
|
*
|
||||||
|
* @throws IOException When file type is unknown
|
||||||
|
*/
|
||||||
|
public function mirror(string $originDir, string $targetDir, \Traversable $iterator = null, array $options = [])
|
||||||
|
{
|
||||||
|
$targetDir = rtrim($targetDir, '/\\');
|
||||||
|
$originDir = rtrim($originDir, '/\\');
|
||||||
|
$originDirLen = \strlen($originDir);
|
||||||
|
|
||||||
|
if (!$this->exists($originDir)) {
|
||||||
|
throw new IOException(sprintf('The origin directory specified "%s" was not found.', $originDir), 0, null, $originDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate in destination folder to remove obsolete entries
|
||||||
|
if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) {
|
||||||
|
$deleteIterator = $iterator;
|
||||||
|
if (null === $deleteIterator) {
|
||||||
|
$flags = \FilesystemIterator::SKIP_DOTS;
|
||||||
|
$deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST);
|
||||||
|
}
|
||||||
|
$targetDirLen = \strlen($targetDir);
|
||||||
|
foreach ($deleteIterator as $file) {
|
||||||
|
$origin = $originDir.substr($file->getPathname(), $targetDirLen);
|
||||||
|
if (!$this->exists($origin)) {
|
||||||
|
$this->remove($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$copyOnWindows = $options['copy_on_windows'] ?? false;
|
||||||
|
|
||||||
|
if (null === $iterator) {
|
||||||
|
$flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS;
|
||||||
|
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->mkdir($targetDir);
|
||||||
|
$filesCreatedWhileMirroring = [];
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->getPathname() === $targetDir || $file->getRealPath() === $targetDir || isset($filesCreatedWhileMirroring[$file->getRealPath()])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$target = $targetDir.substr($file->getPathname(), $originDirLen);
|
||||||
|
$filesCreatedWhileMirroring[$target] = true;
|
||||||
|
|
||||||
|
if (!$copyOnWindows && is_link($file)) {
|
||||||
|
$this->symlink($file->getLinkTarget(), $target);
|
||||||
|
} elseif (is_dir($file)) {
|
||||||
|
$this->mkdir($target);
|
||||||
|
} elseif (is_file($file)) {
|
||||||
|
$this->copy($file, $target, $options['override'] ?? false);
|
||||||
|
} else {
|
||||||
|
throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the file path is an absolute path.
|
||||||
|
*/
|
||||||
|
public function isAbsolutePath(string $file): bool
|
||||||
|
{
|
||||||
|
return '' !== $file && (strspn($file, '/\\', 0, 1)
|
||||||
|
|| (\strlen($file) > 3 && ctype_alpha($file[0])
|
||||||
|
&& ':' === $file[1]
|
||||||
|
&& strspn($file, '/\\', 2, 1)
|
||||||
|
)
|
||||||
|
|| null !== parse_url($file, \PHP_URL_SCHEME)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a temporary file with support for custom stream wrappers.
|
||||||
|
*
|
||||||
|
* @param string $prefix The prefix of the generated temporary filename
|
||||||
|
* Note: Windows uses only the first three characters of prefix
|
||||||
|
* @param string $suffix The suffix of the generated temporary filename
|
||||||
|
*
|
||||||
|
* @return string The new temporary filename (with path), or throw an exception on failure
|
||||||
|
*/
|
||||||
|
public function tempnam(string $dir, string $prefix, string $suffix = ''): string
|
||||||
|
{
|
||||||
|
[$scheme, $hierarchy] = $this->getSchemeAndHierarchy($dir);
|
||||||
|
|
||||||
|
// If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
|
||||||
|
if ((null === $scheme || 'file' === $scheme || 'gs' === $scheme) && '' === $suffix) {
|
||||||
|
// If tempnam failed or no scheme return the filename otherwise prepend the scheme
|
||||||
|
if ($tmpFile = self::box('tempnam', $hierarchy, $prefix)) {
|
||||||
|
if (null !== $scheme && 'gs' !== $scheme) {
|
||||||
|
return $scheme.'://'.$tmpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tmpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException('A temporary file could not be created: '.self::$lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop until we create a valid temp file or have reached 10 attempts
|
||||||
|
for ($i = 0; $i < 10; ++$i) {
|
||||||
|
// Create a unique filename
|
||||||
|
$tmpFile = $dir.'/'.$prefix.uniqid(mt_rand(), true).$suffix;
|
||||||
|
|
||||||
|
// Use fopen instead of file_exists as some streams do not support stat
|
||||||
|
// Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
|
||||||
|
if (!$handle = self::box('fopen', $tmpFile, 'x+')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close the file if it was successfully opened
|
||||||
|
self::box('fclose', $handle);
|
||||||
|
|
||||||
|
return $tmpFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IOException('A temporary file could not be created: '.self::$lastError);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atomically dumps content into a file.
|
||||||
|
*
|
||||||
|
* @param string|resource $content The data to write into the file
|
||||||
|
*
|
||||||
|
* @throws IOException if the file cannot be written to
|
||||||
|
*/
|
||||||
|
public function dumpFile(string $filename, $content)
|
||||||
|
{
|
||||||
|
if (\is_array($content)) {
|
||||||
|
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = \dirname($filename);
|
||||||
|
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
$this->mkdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Will create a temp file with 0600 access rights
|
||||||
|
// when the filesystem supports chmod.
|
||||||
|
$tmpFile = $this->tempnam($dir, basename($filename));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (false === self::box('file_put_contents', $tmpFile, $content)) {
|
||||||
|
throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
self::box('chmod', $tmpFile, file_exists($filename) ? fileperms($filename) : 0666 & ~umask());
|
||||||
|
|
||||||
|
$this->rename($tmpFile, $filename, true);
|
||||||
|
} finally {
|
||||||
|
if (file_exists($tmpFile)) {
|
||||||
|
self::box('unlink', $tmpFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends content to an existing file.
|
||||||
|
*
|
||||||
|
* @param string|resource $content The content to append
|
||||||
|
* @param bool $lock Whether the file should be locked when writing to it
|
||||||
|
*
|
||||||
|
* @throws IOException If the file is not writable
|
||||||
|
*/
|
||||||
|
public function appendToFile(string $filename, $content/* , bool $lock = false */)
|
||||||
|
{
|
||||||
|
if (\is_array($content)) {
|
||||||
|
throw new \TypeError(sprintf('Argument 2 passed to "%s()" must be string or resource, array given.', __METHOD__));
|
||||||
|
}
|
||||||
|
|
||||||
|
$dir = \dirname($filename);
|
||||||
|
|
||||||
|
if (!is_dir($dir)) {
|
||||||
|
$this->mkdir($dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lock = \func_num_args() > 2 && func_get_arg(2);
|
||||||
|
|
||||||
|
if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND | ($lock ? \LOCK_EX : 0))) {
|
||||||
|
throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function toIterable(string|iterable $files): iterable
|
||||||
|
{
|
||||||
|
return is_iterable($files) ? $files : [$files];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a 2-tuple of scheme (may be null) and hierarchical part of a filename (e.g. file:///tmp -> [file, tmp]).
|
||||||
|
*/
|
||||||
|
private function getSchemeAndHierarchy(string $filename): array
|
||||||
|
{
|
||||||
|
$components = explode('://', $filename, 2);
|
||||||
|
|
||||||
|
return 2 === \count($components) ? [$components[0], $components[1]] : [null, $components[0]];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function assertFunctionExists(string $func): void
|
||||||
|
{
|
||||||
|
if (!\function_exists($func)) {
|
||||||
|
throw new IOException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function box(string $func, mixed ...$args): mixed
|
||||||
|
{
|
||||||
|
self::assertFunctionExists($func);
|
||||||
|
|
||||||
|
self::$lastError = null;
|
||||||
|
set_error_handler(__CLASS__.'::handleError');
|
||||||
|
try {
|
||||||
|
return $func(...$args);
|
||||||
|
} finally {
|
||||||
|
restore_error_handler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
public static function handleError(int $type, string $msg)
|
||||||
|
{
|
||||||
|
self::$lastError = $msg;
|
||||||
|
}
|
||||||
|
}
|
19
src/ncc/ThirdParty/Symfony/Filesystem/LICENSE
vendored
Normal file
19
src/ncc/ThirdParty/Symfony/Filesystem/LICENSE
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
Copyright (c) 2004-2022 Fabien Potencier
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is furnished
|
||||||
|
to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
819
src/ncc/ThirdParty/Symfony/Filesystem/Path.php
vendored
Normal file
819
src/ncc/ThirdParty/Symfony/Filesystem/Path.php
vendored
Normal file
|
@ -0,0 +1,819 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace ncc\ThirdParty\Symfony\Filesystem;
|
||||||
|
|
||||||
|
use ncc\ThirdParty\Symfony\Filesystem\Exception\InvalidArgumentException;
|
||||||
|
use ncc\ThirdParty\Symfony\Filesystem\Exception\RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contains utility methods for handling path strings.
|
||||||
|
*
|
||||||
|
* The methods in this class are able to deal with both UNIX and Windows paths
|
||||||
|
* with both forward and backward slashes. All methods return normalized parts
|
||||||
|
* containing only forward slashes and no excess "." and ".." segments.
|
||||||
|
*
|
||||||
|
* @author Bernhard Schussek <bschussek@gmail.com>
|
||||||
|
* @author Thomas Schulz <mail@king2500.net>
|
||||||
|
* @author Théo Fidry <theo.fidry@gmail.com>
|
||||||
|
*/
|
||||||
|
final class Path
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The number of buffer entries that triggers a cleanup operation.
|
||||||
|
*/
|
||||||
|
private const CLEANUP_THRESHOLD = 1250;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The buffer size after the cleanup operation.
|
||||||
|
*/
|
||||||
|
private const CLEANUP_SIZE = 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Buffers input/output of {@link canonicalize()}.
|
||||||
|
*
|
||||||
|
* @var array<string, string>
|
||||||
|
*/
|
||||||
|
private static $buffer = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private static $bufferSize = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canonicalizes the given path.
|
||||||
|
*
|
||||||
|
* During normalization, all slashes are replaced by forward slashes ("/").
|
||||||
|
* Furthermore, all "." and ".." segments are removed as far as possible.
|
||||||
|
* ".." segments at the beginning of relative paths are not removed.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* echo Path::canonicalize("\symfony\puli\..\css\style.css");
|
||||||
|
* // => /symfony/css/style.css
|
||||||
|
*
|
||||||
|
* echo Path::canonicalize("../css/./style.css");
|
||||||
|
* // => ../css/style.css
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* This method is able to deal with both UNIX and Windows paths.
|
||||||
|
*/
|
||||||
|
public static function canonicalize(string $path): string
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// This method is called by many other methods in this class. Buffer
|
||||||
|
// the canonicalized paths to make up for the severe performance
|
||||||
|
// decrease.
|
||||||
|
if (isset(self::$buffer[$path])) {
|
||||||
|
return self::$buffer[$path];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace "~" with user's home directory.
|
||||||
|
if ('~' === $path[0]) {
|
||||||
|
$path = self::getHomeDirectory().mb_substr($path, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = self::normalize($path);
|
||||||
|
|
||||||
|
[$root, $pathWithoutRoot] = self::split($path);
|
||||||
|
|
||||||
|
$canonicalParts = self::findCanonicalParts($root, $pathWithoutRoot);
|
||||||
|
|
||||||
|
// Add the root directory again
|
||||||
|
self::$buffer[$path] = $canonicalPath = $root.implode('/', $canonicalParts);
|
||||||
|
++self::$bufferSize;
|
||||||
|
|
||||||
|
// Clean up regularly to prevent memory leaks
|
||||||
|
if (self::$bufferSize > self::CLEANUP_THRESHOLD) {
|
||||||
|
self::$buffer = \array_slice(self::$buffer, -self::CLEANUP_SIZE, null, true);
|
||||||
|
self::$bufferSize = self::CLEANUP_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $canonicalPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the given path.
|
||||||
|
*
|
||||||
|
* During normalization, all slashes are replaced by forward slashes ("/").
|
||||||
|
* Contrary to {@link canonicalize()}, this method does not remove invalid
|
||||||
|
* or dot path segments. Consequently, it is much more efficient and should
|
||||||
|
* be used whenever the given path is known to be a valid, absolute system
|
||||||
|
* path.
|
||||||
|
*
|
||||||
|
* This method is able to deal with both UNIX and Windows paths.
|
||||||
|
*/
|
||||||
|
public static function normalize(string $path): string
|
||||||
|
{
|
||||||
|
return str_replace('\\', '/', $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the directory part of the path.
|
||||||
|
*
|
||||||
|
* This method is similar to PHP's dirname(), but handles various cases
|
||||||
|
* where dirname() returns a weird result:
|
||||||
|
*
|
||||||
|
* - dirname() does not accept backslashes on UNIX
|
||||||
|
* - dirname("C:/symfony") returns "C:", not "C:/"
|
||||||
|
* - dirname("C:/") returns ".", not "C:/"
|
||||||
|
* - dirname("C:") returns ".", not "C:/"
|
||||||
|
* - dirname("symfony") returns ".", not ""
|
||||||
|
* - dirname() does not canonicalize the result
|
||||||
|
*
|
||||||
|
* This method fixes these shortcomings and behaves like dirname()
|
||||||
|
* otherwise.
|
||||||
|
*
|
||||||
|
* The result is a canonical path.
|
||||||
|
*
|
||||||
|
* @return string The canonical directory part. Returns the root directory
|
||||||
|
* if the root directory is passed. Returns an empty string
|
||||||
|
* if a relative path is passed that contains no slashes.
|
||||||
|
* Returns an empty string if an empty string is passed.
|
||||||
|
*/
|
||||||
|
public static function getDirectory(string $path): string
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = self::canonicalize($path);
|
||||||
|
|
||||||
|
// Maintain scheme
|
||||||
|
if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
|
||||||
|
$scheme = mb_substr($path, 0, $schemeSeparatorPosition + 3);
|
||||||
|
$path = mb_substr($path, $schemeSeparatorPosition + 3);
|
||||||
|
} else {
|
||||||
|
$scheme = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false === ($dirSeparatorPosition = strrpos($path, '/'))) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory equals root directory "/"
|
||||||
|
if (0 === $dirSeparatorPosition) {
|
||||||
|
return $scheme.'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory equals Windows root "C:/"
|
||||||
|
if (2 === $dirSeparatorPosition && ctype_alpha($path[0]) && ':' === $path[1]) {
|
||||||
|
return $scheme.mb_substr($path, 0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scheme.mb_substr($path, 0, $dirSeparatorPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns canonical path of the user's home directory.
|
||||||
|
*
|
||||||
|
* Supported operating systems:
|
||||||
|
*
|
||||||
|
* - UNIX
|
||||||
|
* - Windows8 and upper
|
||||||
|
*
|
||||||
|
* If your operating system or environment isn't supported, an exception is thrown.
|
||||||
|
*
|
||||||
|
* The result is a canonical path.
|
||||||
|
*
|
||||||
|
* @throws RuntimeException If your operating system or environment isn't supported
|
||||||
|
*/
|
||||||
|
public static function getHomeDirectory(): string
|
||||||
|
{
|
||||||
|
// For UNIX support
|
||||||
|
if (getenv('HOME')) {
|
||||||
|
return self::canonicalize(getenv('HOME'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// For >= Windows8 support
|
||||||
|
if (getenv('HOMEDRIVE') && getenv('HOMEPATH')) {
|
||||||
|
return self::canonicalize(getenv('HOMEDRIVE').getenv('HOMEPATH'));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new RuntimeException("Cannot find the home directory path: Your environment or operating system isn't supported.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the root directory of a path.
|
||||||
|
*
|
||||||
|
* The result is a canonical path.
|
||||||
|
*
|
||||||
|
* @return string The canonical root directory. Returns an empty string if
|
||||||
|
* the given path is relative or empty.
|
||||||
|
*/
|
||||||
|
public static function getRoot(string $path): string
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maintain scheme
|
||||||
|
if (false !== ($schemeSeparatorPosition = strpos($path, '://'))) {
|
||||||
|
$scheme = substr($path, 0, $schemeSeparatorPosition + 3);
|
||||||
|
$path = substr($path, $schemeSeparatorPosition + 3);
|
||||||
|
} else {
|
||||||
|
$scheme = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstCharacter = $path[0];
|
||||||
|
|
||||||
|
// UNIX root "/" or "\" (Windows style)
|
||||||
|
if ('/' === $firstCharacter || '\\' === $firstCharacter) {
|
||||||
|
return $scheme.'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = mb_strlen($path);
|
||||||
|
|
||||||
|
// Windows root
|
||||||
|
if ($length > 1 && ':' === $path[1] && ctype_alpha($firstCharacter)) {
|
||||||
|
// Special case: "C:"
|
||||||
|
if (2 === $length) {
|
||||||
|
return $scheme.$path.'/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal case: "C:/ or "C:\"
|
||||||
|
if ('/' === $path[2] || '\\' === $path[2]) {
|
||||||
|
return $scheme.$firstCharacter.$path[1].'/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the file name without the extension from a file path.
|
||||||
|
*
|
||||||
|
* @param string|null $extension if specified, only that extension is cut
|
||||||
|
* off (may contain leading dot)
|
||||||
|
*/
|
||||||
|
public static function getFilenameWithoutExtension(string $path, string $extension = null): string
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $extension) {
|
||||||
|
// remove extension and trailing dot
|
||||||
|
return rtrim(basename($path, $extension), '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathinfo($path, \PATHINFO_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the extension from a file path (without leading dot).
|
||||||
|
*
|
||||||
|
* @param bool $forceLowerCase forces the extension to be lower-case
|
||||||
|
*/
|
||||||
|
public static function getExtension(string $path, bool $forceLowerCase = false): string
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$extension = pathinfo($path, \PATHINFO_EXTENSION);
|
||||||
|
|
||||||
|
if ($forceLowerCase) {
|
||||||
|
$extension = self::toLower($extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the path has an (or the specified) extension.
|
||||||
|
*
|
||||||
|
* @param string $path the path string
|
||||||
|
* @param string|string[]|null $extensions if null or not provided, checks if
|
||||||
|
* an extension exists, otherwise
|
||||||
|
* checks for the specified extension
|
||||||
|
* or array of extensions (with or
|
||||||
|
* without leading dot)
|
||||||
|
* @param bool $ignoreCase whether to ignore case-sensitivity
|
||||||
|
*/
|
||||||
|
public static function hasExtension(string $path, $extensions = null, bool $ignoreCase = false): bool
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actualExtension = self::getExtension($path, $ignoreCase);
|
||||||
|
|
||||||
|
// Only check if path has any extension
|
||||||
|
if ([] === $extensions || null === $extensions) {
|
||||||
|
return '' !== $actualExtension;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (\is_string($extensions)) {
|
||||||
|
$extensions = [$extensions];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($extensions as $key => $extension) {
|
||||||
|
if ($ignoreCase) {
|
||||||
|
$extension = self::toLower($extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove leading '.' in extensions array
|
||||||
|
$extensions[$key] = ltrim($extension, '.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return \in_array($actualExtension, $extensions, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the extension of a path string.
|
||||||
|
*
|
||||||
|
* @param string $path The path string with filename.ext to change.
|
||||||
|
* @param string $extension new extension (with or without leading dot)
|
||||||
|
*
|
||||||
|
* @return string the path string with new file extension
|
||||||
|
*/
|
||||||
|
public static function changeExtension(string $path, string $extension): string
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$actualExtension = self::getExtension($path);
|
||||||
|
$extension = ltrim($extension, '.');
|
||||||
|
|
||||||
|
// No extension for paths
|
||||||
|
if ('/' === mb_substr($path, -1)) {
|
||||||
|
return $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No actual extension in path
|
||||||
|
if (empty($actualExtension)) {
|
||||||
|
return $path.('.' === mb_substr($path, -1) ? '' : '.').$extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb_substr($path, 0, -mb_strlen($actualExtension)).$extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isAbsolute(string $path): bool
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip scheme
|
||||||
|
if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
|
||||||
|
$path = mb_substr($path, $schemeSeparatorPosition + 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
$firstCharacter = $path[0];
|
||||||
|
|
||||||
|
// UNIX root "/" or "\" (Windows style)
|
||||||
|
if ('/' === $firstCharacter || '\\' === $firstCharacter) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows root
|
||||||
|
if (mb_strlen($path) > 1 && ctype_alpha($firstCharacter) && ':' === $path[1]) {
|
||||||
|
// Special case: "C:"
|
||||||
|
if (2 === mb_strlen($path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal case: "C:/ or "C:\"
|
||||||
|
if ('/' === $path[2] || '\\' === $path[2]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isRelative(string $path): bool
|
||||||
|
{
|
||||||
|
return !self::isAbsolute($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a relative path into an absolute path in canonical form.
|
||||||
|
*
|
||||||
|
* Usually, the relative path is appended to the given base path. Dot
|
||||||
|
* segments ("." and "..") are removed/collapsed and all slashes turned
|
||||||
|
* into forward slashes.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* echo Path::makeAbsolute("../style.css", "/symfony/puli/css");
|
||||||
|
* // => /symfony/puli/style.css
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If an absolute path is passed, that path is returned unless its root
|
||||||
|
* directory is different than the one of the base path. In that case, an
|
||||||
|
* exception is thrown.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* Path::makeAbsolute("/style.css", "/symfony/puli/css");
|
||||||
|
* // => /style.css
|
||||||
|
*
|
||||||
|
* Path::makeAbsolute("C:/style.css", "C:/symfony/puli/css");
|
||||||
|
* // => C:/style.css
|
||||||
|
*
|
||||||
|
* Path::makeAbsolute("C:/style.css", "/symfony/puli/css");
|
||||||
|
* // InvalidArgumentException
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If the base path is not an absolute path, an exception is thrown.
|
||||||
|
*
|
||||||
|
* The result is a canonical path.
|
||||||
|
*
|
||||||
|
* @param string $basePath an absolute base path
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException if the base path is not absolute or if
|
||||||
|
* the given path is an absolute path with
|
||||||
|
* a different root than the base path
|
||||||
|
*/
|
||||||
|
public static function makeAbsolute(string $path, string $basePath): string
|
||||||
|
{
|
||||||
|
if ('' === $basePath) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The base path must be a non-empty string. Got: "%s".', $basePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!self::isAbsolute($basePath)) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The base path "%s" is not an absolute path.', $basePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isAbsolute($path)) {
|
||||||
|
return self::canonicalize($path);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (false !== ($schemeSeparatorPosition = mb_strpos($basePath, '://'))) {
|
||||||
|
$scheme = mb_substr($basePath, 0, $schemeSeparatorPosition + 3);
|
||||||
|
$basePath = mb_substr($basePath, $schemeSeparatorPosition + 3);
|
||||||
|
} else {
|
||||||
|
$scheme = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $scheme.self::canonicalize(rtrim($basePath, '/\\').'/'.$path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turns a path into a relative path.
|
||||||
|
*
|
||||||
|
* The relative path is created relative to the given base path:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* echo Path::makeRelative("/symfony/style.css", "/symfony/puli");
|
||||||
|
* // => ../style.css
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If a relative path is passed and the base path is absolute, the relative
|
||||||
|
* path is returned unchanged:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* Path::makeRelative("style.css", "/symfony/puli/css");
|
||||||
|
* // => style.css
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If both paths are relative, the relative path is created with the
|
||||||
|
* assumption that both paths are relative to the same directory:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* Path::makeRelative("style.css", "symfony/puli/css");
|
||||||
|
* // => ../../../style.css
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If both paths are absolute, their root directory must be the same,
|
||||||
|
* otherwise an exception is thrown:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* Path::makeRelative("C:/symfony/style.css", "/symfony/puli");
|
||||||
|
* // InvalidArgumentException
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If the passed path is absolute, but the base path is not, an exception
|
||||||
|
* is thrown as well:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* Path::makeRelative("/symfony/style.css", "symfony/puli");
|
||||||
|
* // InvalidArgumentException
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If the base path is not an absolute path, an exception is thrown.
|
||||||
|
*
|
||||||
|
* The result is a canonical path.
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException if the base path is not absolute or if
|
||||||
|
* the given path has a different root
|
||||||
|
* than the base path
|
||||||
|
*/
|
||||||
|
public static function makeRelative(string $path, string $basePath): string
|
||||||
|
{
|
||||||
|
$path = self::canonicalize($path);
|
||||||
|
$basePath = self::canonicalize($basePath);
|
||||||
|
|
||||||
|
[$root, $relativePath] = self::split($path);
|
||||||
|
[$baseRoot, $relativeBasePath] = self::split($basePath);
|
||||||
|
|
||||||
|
// If the base path is given as absolute path and the path is already
|
||||||
|
// relative, consider it to be relative to the given absolute path
|
||||||
|
// already
|
||||||
|
if ('' === $root && '' !== $baseRoot) {
|
||||||
|
// If base path is already in its root
|
||||||
|
if ('' === $relativeBasePath) {
|
||||||
|
$relativePath = ltrim($relativePath, './\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the passed path is absolute, but the base path is not, we
|
||||||
|
// cannot generate a relative path
|
||||||
|
if ('' !== $root && '' === $baseRoot) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The absolute path "%s" cannot be made relative to the relative path "%s". You should provide an absolute base path instead.', $path, $basePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail if the roots of the two paths are different
|
||||||
|
if ($baseRoot && $root !== $baseRoot) {
|
||||||
|
throw new InvalidArgumentException(sprintf('The path "%s" cannot be made relative to "%s", because they have different roots ("%s" and "%s").', $path, $basePath, $root, $baseRoot));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('' === $relativeBasePath) {
|
||||||
|
return $relativePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a "../../" prefix with as many "../" parts as necessary
|
||||||
|
$parts = explode('/', $relativePath);
|
||||||
|
$baseParts = explode('/', $relativeBasePath);
|
||||||
|
$dotDotPrefix = '';
|
||||||
|
|
||||||
|
// Once we found a non-matching part in the prefix, we need to add
|
||||||
|
// "../" parts for all remaining parts
|
||||||
|
$match = true;
|
||||||
|
|
||||||
|
foreach ($baseParts as $index => $basePart) {
|
||||||
|
if ($match && isset($parts[$index]) && $basePart === $parts[$index]) {
|
||||||
|
unset($parts[$index]);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$match = false;
|
||||||
|
$dotDotPrefix .= '../';
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($dotDotPrefix.implode('/', $parts), '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the given path is on the local filesystem.
|
||||||
|
*/
|
||||||
|
public static function isLocal(string $path): bool
|
||||||
|
{
|
||||||
|
return '' !== $path && !str_contains($path, '://');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the longest common base path in canonical form of a set of paths or
|
||||||
|
* `null` if the paths are on different Windows partitions.
|
||||||
|
*
|
||||||
|
* Dot segments ("." and "..") are removed/collapsed and all slashes turned
|
||||||
|
* into forward slashes.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $basePath = Path::getLongestCommonBasePath(
|
||||||
|
* '/symfony/css/style.css',
|
||||||
|
* '/symfony/css/..'
|
||||||
|
* );
|
||||||
|
* // => /symfony
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The root is returned if no common base path can be found:
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $basePath = Path::getLongestCommonBasePath(
|
||||||
|
* '/symfony/css/style.css',
|
||||||
|
* '/puli/css/..'
|
||||||
|
* );
|
||||||
|
* // => /
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* If the paths are located on different Windows partitions, `null` is
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* $basePath = Path::getLongestCommonBasePath(
|
||||||
|
* 'C:/symfony/css/style.css',
|
||||||
|
* 'D:/symfony/css/..'
|
||||||
|
* );
|
||||||
|
* // => null
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public static function getLongestCommonBasePath(string ...$paths): ?string
|
||||||
|
{
|
||||||
|
[$bpRoot, $basePath] = self::split(self::canonicalize(reset($paths)));
|
||||||
|
|
||||||
|
for (next($paths); null !== key($paths) && '' !== $basePath; next($paths)) {
|
||||||
|
[$root, $path] = self::split(self::canonicalize(current($paths)));
|
||||||
|
|
||||||
|
// If we deal with different roots (e.g. C:/ vs. D:/), it's time
|
||||||
|
// to quit
|
||||||
|
if ($root !== $bpRoot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make the base path shorter until it fits into path
|
||||||
|
while (true) {
|
||||||
|
if ('.' === $basePath) {
|
||||||
|
// No more base paths
|
||||||
|
$basePath = '';
|
||||||
|
|
||||||
|
// next path
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent false positives for common prefixes
|
||||||
|
// see isBasePath()
|
||||||
|
if (str_starts_with($path.'/', $basePath.'/')) {
|
||||||
|
// next path
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$basePath = \dirname($basePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $bpRoot.$basePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Joins two or more path strings into a canonical path.
|
||||||
|
*/
|
||||||
|
public static function join(string ...$paths): string
|
||||||
|
{
|
||||||
|
$finalPath = null;
|
||||||
|
$wasScheme = false;
|
||||||
|
|
||||||
|
foreach ($paths as $path) {
|
||||||
|
if ('' === $path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $finalPath) {
|
||||||
|
// For first part we keep slashes, like '/top', 'C:\' or 'phar://'
|
||||||
|
$finalPath = $path;
|
||||||
|
$wasScheme = str_contains($path, '://');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add slash if previous part didn't end with '/' or '\'
|
||||||
|
if (!\in_array(mb_substr($finalPath, -1), ['/', '\\'])) {
|
||||||
|
$finalPath .= '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If first part included a scheme like 'phar://' we allow \current part to start with '/', otherwise trim
|
||||||
|
$finalPath .= $wasScheme ? $path : ltrim($path, '/');
|
||||||
|
$wasScheme = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null === $finalPath) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::canonicalize($finalPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether a path is a base path of another path.
|
||||||
|
*
|
||||||
|
* Dot segments ("." and "..") are removed/collapsed and all slashes turned
|
||||||
|
* into forward slashes.
|
||||||
|
*
|
||||||
|
* ```php
|
||||||
|
* Path::isBasePath('/symfony', '/symfony/css');
|
||||||
|
* // => true
|
||||||
|
*
|
||||||
|
* Path::isBasePath('/symfony', '/symfony');
|
||||||
|
* // => true
|
||||||
|
*
|
||||||
|
* Path::isBasePath('/symfony', '/symfony/..');
|
||||||
|
* // => false
|
||||||
|
*
|
||||||
|
* Path::isBasePath('/symfony', '/puli');
|
||||||
|
* // => false
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
public static function isBasePath(string $basePath, string $ofPath): bool
|
||||||
|
{
|
||||||
|
$basePath = self::canonicalize($basePath);
|
||||||
|
$ofPath = self::canonicalize($ofPath);
|
||||||
|
|
||||||
|
// Append slashes to prevent false positives when two paths have
|
||||||
|
// a common prefix, for example /base/foo and /base/foobar.
|
||||||
|
// Don't append a slash for the root "/", because then that root
|
||||||
|
// won't be discovered as common prefix ("//" is not a prefix of
|
||||||
|
// "/foobar/").
|
||||||
|
return str_starts_with($ofPath.'/', rtrim($basePath, '/').'/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return non-empty-string[]
|
||||||
|
*/
|
||||||
|
private static function findCanonicalParts(string $root, string $pathWithoutRoot): array
|
||||||
|
{
|
||||||
|
$parts = explode('/', $pathWithoutRoot);
|
||||||
|
|
||||||
|
$canonicalParts = [];
|
||||||
|
|
||||||
|
// Collapse "." and "..", if possible
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
if ('.' === $part || '' === $part) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collapse ".." with the previous part, if one exists
|
||||||
|
// Don't collapse ".." if the previous part is also ".."
|
||||||
|
if ('..' === $part && \count($canonicalParts) > 0 && '..' !== $canonicalParts[\count($canonicalParts) - 1]) {
|
||||||
|
array_pop($canonicalParts);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only add ".." prefixes for relative paths
|
||||||
|
if ('..' !== $part || '' === $root) {
|
||||||
|
$canonicalParts[] = $part;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $canonicalParts;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a canonical path into its root directory and the remainder.
|
||||||
|
*
|
||||||
|
* If the path has no root directory, an empty root directory will be
|
||||||
|
* returned.
|
||||||
|
*
|
||||||
|
* If the root directory is a Windows style partition, the resulting root
|
||||||
|
* will always contain a trailing slash.
|
||||||
|
*
|
||||||
|
* list ($root, $path) = Path::split("C:/symfony")
|
||||||
|
* // => ["C:/", "symfony"]
|
||||||
|
*
|
||||||
|
* list ($root, $path) = Path::split("C:")
|
||||||
|
* // => ["C:/", ""]
|
||||||
|
*
|
||||||
|
* @return array{string, string} an array with the root directory and the remaining relative path
|
||||||
|
*/
|
||||||
|
private static function split(string $path): array
|
||||||
|
{
|
||||||
|
if ('' === $path) {
|
||||||
|
return ['', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember scheme as part of the root, if any
|
||||||
|
if (false !== ($schemeSeparatorPosition = mb_strpos($path, '://'))) {
|
||||||
|
$root = mb_substr($path, 0, $schemeSeparatorPosition + 3);
|
||||||
|
$path = mb_substr($path, $schemeSeparatorPosition + 3);
|
||||||
|
} else {
|
||||||
|
$root = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$length = mb_strlen($path);
|
||||||
|
|
||||||
|
// Remove and remember root directory
|
||||||
|
if (str_starts_with($path, '/')) {
|
||||||
|
$root .= '/';
|
||||||
|
$path = $length > 1 ? mb_substr($path, 1) : '';
|
||||||
|
} elseif ($length > 1 && ctype_alpha($path[0]) && ':' === $path[1]) {
|
||||||
|
if (2 === $length) {
|
||||||
|
// Windows special case: "C:"
|
||||||
|
$root .= $path.'/';
|
||||||
|
$path = '';
|
||||||
|
} elseif ('/' === $path[2]) {
|
||||||
|
// Windows normal case: "C:/"..
|
||||||
|
$root .= mb_substr($path, 0, 3);
|
||||||
|
$path = $length > 3 ? mb_substr($path, 3) : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$root, $path];
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function toLower(string $string): string
|
||||||
|
{
|
||||||
|
if (false !== $encoding = mb_detect_encoding($string)) {
|
||||||
|
return mb_strtolower($string, $encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
return strtolower($string, $encoding);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
13
src/ncc/ThirdParty/Symfony/Filesystem/README.md
vendored
Normal file
13
src/ncc/ThirdParty/Symfony/Filesystem/README.md
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
Filesystem Component
|
||||||
|
====================
|
||||||
|
|
||||||
|
The Filesystem component provides basic utilities for the filesystem.
|
||||||
|
|
||||||
|
Resources
|
||||||
|
---------
|
||||||
|
|
||||||
|
* [Documentation](https://symfony.com/doc/current/components/filesystem.html)
|
||||||
|
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
|
||||||
|
* [Report issues](https://github.com/symfony/symfony/issues) and
|
||||||
|
[send Pull Requests](https://github.com/symfony/symfony/pulls)
|
||||||
|
in the [main Symfony repository](https://github.com/symfony/symfony)
|
|
@ -0,0 +1 @@
|
||||||
|
6.1.3
|
|
@ -22,6 +22,10 @@
|
||||||
{
|
{
|
||||||
"vendor": "Symfony",
|
"vendor": "Symfony",
|
||||||
"package_name": "polyfill-uid"
|
"package_name": "polyfill-uid"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"vendor": "Symfony",
|
||||||
|
"package_name": "Filesystem"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"update_source": null
|
"update_source": null
|
||||||
|
|
Loading…
Add table
Reference in a new issue