2021-05-10 00:23:30 +00:00
< ? 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 .
*/
2021-07-06 08:19:07 +00:00
namespace RectorPrefix20210706\Symfony\Component\Filesystem ;
2021-05-10 00:23:30 +00:00
2021-07-06 08:19:07 +00:00
use RectorPrefix20210706\Symfony\Component\Filesystem\Exception\FileNotFoundException ;
use RectorPrefix20210706\Symfony\Component\Filesystem\Exception\InvalidArgumentException ;
use RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ;
2021-05-10 00:23:30 +00:00
/**
* 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
2021-07-05 22:50:18 +00:00
* @ param string $originFile
* @ param string $targetFile
* @ param bool $overwriteNewerFiles
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function copy ( $originFile , $targetFile , $overwriteNewerFiles = \false )
2021-05-10 00:23:30 +00:00
{
$originIsLocal = \stream_is_local ( $originFile ) || 0 === \stripos ( $originFile , 'file://' );
if ( $originIsLocal && ! \is_file ( $originFile )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\FileNotFoundException ( \sprintf ( 'Failed to copy "%s" because file does not exist.' , $originFile ), 0 , null , $originFile );
2021-05-10 00:23:30 +00:00
}
$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
2021-06-01 12:39:02 +00:00
if ( ! ( $source = self :: box ( 'fopen' , $originFile , 'r' ))) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to copy "%s" to "%s" because source file could not be opened for reading: ' , $originFile , $targetFile ) . self :: $lastError , 0 , null , $originFile );
2021-05-10 00:23:30 +00:00
}
// Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
2021-06-01 12:39:02 +00:00
if ( ! ( $target = self :: box ( 'fopen' , $targetFile , 'w' , \false , \stream_context_create ([ 'ftp' => [ 'overwrite' => \true ]])))) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to copy "%s" to "%s" because target file could not be opened for writing: ' , $originFile , $targetFile ) . self :: $lastError , 0 , null , $originFile );
2021-05-10 00:23:30 +00:00
}
$bytesCopied = \stream_copy_to_stream ( $source , $target );
\fclose ( $source );
\fclose ( $target );
unset ( $source , $target );
if ( ! \is_file ( $targetFile )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to copy "%s" to "%s".' , $originFile , $targetFile ), 0 , null , $originFile );
2021-05-10 00:23:30 +00:00
}
if ( $originIsLocal ) {
// Like `cp`, preserve executable permission bits
2021-06-01 12:39:02 +00:00
self :: box ( 'chmod' , $targetFile , \fileperms ( $targetFile ) | \fileperms ( $originFile ) & 0111 );
2021-05-10 00:23:30 +00:00
if ( $bytesCopied !== ( $bytesOrigin = \filesize ( $originFile ))) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).' , $originFile , $targetFile , $bytesCopied , $bytesOrigin ), 0 , null , $originFile );
2021-05-10 00:23:30 +00:00
}
}
}
}
/**
* Creates a directory recursively .
*
* @ param string | iterable $dirs The directory path
*
* @ throws IOException On any directory creation failure
2021-07-05 22:50:18 +00:00
* @ param int $mode
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function mkdir ( $dirs , $mode = 0777 )
2021-05-10 00:23:30 +00:00
{
foreach ( $this -> toIterable ( $dirs ) as $dir ) {
if ( \is_dir ( $dir )) {
continue ;
}
2021-06-01 12:39:02 +00:00
if ( ! self :: box ( 'mkdir' , $dir , $mode , \true ) && ! \is_dir ( $dir )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to create "%s": ' , $dir ) . self :: $lastError , 0 , null , $dir );
2021-05-10 00:23:30 +00:00
}
}
}
/**
* Checks the existence of files or directories .
*
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to check
*
* @ return bool true if the file exists , false otherwise
*/
public function exists ( $files )
{
$maxPathLength = \PHP_MAXPATHLEN - 2 ;
foreach ( $this -> toIterable ( $files ) as $file ) {
if ( \strlen ( $file ) > $maxPathLength ) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Could not check if file exist because path length exceeds %d characters.' , $maxPathLength ), 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
if ( ! \file_exists ( $file )) {
return \false ;
}
}
return \true ;
}
/**
* Sets access and modification time of file .
*
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to create
* @ 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
*/
2021-07-05 22:50:18 +00:00
public function touch ( $files , $time = null , $atime = null )
2021-05-10 00:23:30 +00:00
{
foreach ( $this -> toIterable ( $files ) as $file ) {
2021-06-01 12:39:02 +00:00
if ( ! ( $time ? self :: box ( 'touch' , $file , $time , $atime ) : self :: box ( 'touch' , $file ))) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to touch "%s": ' , $file ) . self :: $lastError , 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
}
}
/**
* Removes files or directories .
*
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to remove
*
* @ throws IOException When removal fails
*/
public function remove ( $files )
{
if ( $files instanceof \Traversable ) {
$files = \iterator_to_array ( $files , \false );
} elseif ( ! \is_array ( $files )) {
$files = [ $files ];
}
2021-06-01 12:39:02 +00:00
self :: doRemove ( $files , \false );
}
2021-07-05 23:06:30 +00:00
private static function doRemove ( array $files , bool $isRecursive ) : void
2021-06-01 12:39:02 +00:00
{
2021-05-10 00:23:30 +00:00
$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 )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to remove symlink "%s": ' , $file ) . self :: $lastError );
2021-05-10 00:23:30 +00:00
}
} elseif ( \is_dir ( $file )) {
2021-06-01 12:39:02 +00:00
if ( ! $isRecursive ) {
$tmpName = \dirname ( \realpath ( $file )) . '/.' . \strrev ( \strtr ( \base64_encode ( \random_bytes ( 2 )), '/=' , '-.' ));
if ( \file_exists ( $tmpName )) {
try {
self :: doRemove ([ $tmpName ], \true );
2021-07-06 08:19:07 +00:00
} catch ( \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException $e ) {
2021-06-01 12:39:02 +00:00
}
}
if ( ! \file_exists ( $tmpName ) && self :: box ( 'rename' , $file , $tmpName )) {
$origFile = $file ;
$file = $tmpName ;
} else {
$origFile = null ;
}
}
$files = new \FilesystemIterator ( $file , \FilesystemIterator :: CURRENT_AS_PATHNAME | \FilesystemIterator :: SKIP_DOTS );
self :: doRemove ( \iterator_to_array ( $files , \true ), \true );
if ( ! self :: box ( 'rmdir' , $file ) && \file_exists ( $file ) && ! $isRecursive ) {
$lastError = self :: $lastError ;
if ( null !== $origFile && self :: box ( 'rename' , $file , $origFile )) {
$file = $origFile ;
}
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to remove directory "%s": ' , $file ) . $lastError );
2021-05-10 00:23:30 +00:00
}
} elseif ( ! self :: box ( 'unlink' , $file ) && ( \false !== \strpos ( self :: $lastError , 'Permission denied' ) || \file_exists ( $file ))) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to remove file "%s": ' , $file ) . self :: $lastError );
2021-05-10 00:23:30 +00:00
}
}
}
/**
* Change mode for an array of files or directories .
*
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to change mode
* @ 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
*/
2021-07-05 22:50:18 +00:00
public function chmod ( $files , $mode , $umask = 00 , $recursive = \false )
2021-05-10 00:23:30 +00:00
{
foreach ( $this -> toIterable ( $files ) as $file ) {
2021-06-01 12:39:02 +00:00
if (( \PHP_VERSION_ID < 80000 || \is_int ( $mode )) && ! self :: box ( 'chmod' , $file , $mode & ~ $umask )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to chmod file "%s": ' , $file ) . self :: $lastError , 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
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 | iterable $files A filename , an array of files , or a \Traversable instance to change owner
* @ 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
*/
2021-07-05 22:50:18 +00:00
public function chown ( $files , $user , $recursive = \false )
2021-05-10 00:23:30 +00:00
{
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' )) {
2021-06-01 12:39:02 +00:00
if ( ! self :: box ( 'lchown' , $file , $user )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to chown file "%s": ' , $file ) . self :: $lastError , 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
} else {
2021-06-01 12:39:02 +00:00
if ( ! self :: box ( 'chown' , $file , $user )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to chown file "%s": ' , $file ) . self :: $lastError , 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
}
}
}
/**
* Change the group of an array of files or directories .
*
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to change group
* @ 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
*/
2021-07-05 22:50:18 +00:00
public function chgrp ( $files , $group , $recursive = \false )
2021-05-10 00:23:30 +00:00
{
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' )) {
2021-06-01 12:39:02 +00:00
if ( ! self :: box ( 'lchgrp' , $file , $group )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to chgrp file "%s": ' , $file ) . self :: $lastError , 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
} else {
2021-06-01 12:39:02 +00:00
if ( ! self :: box ( 'chgrp' , $file , $group )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to chgrp file "%s": ' , $file ) . self :: $lastError , 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
}
}
}
/**
* Renames a file or a directory .
*
* @ throws IOException When target file or directory already exists
* @ throws IOException When origin cannot be renamed
2021-07-05 22:50:18 +00:00
* @ param string $origin
* @ param string $target
* @ param bool $overwrite
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function rename ( $origin , $target , $overwrite = \false )
2021-05-10 00:23:30 +00:00
{
// we check that target does not exist
if ( ! $overwrite && $this -> isReadable ( $target )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Cannot rename because the target "%s" already exists.' , $target ), 0 , null , $target );
2021-05-10 00:23:30 +00:00
}
2021-06-01 12:39:02 +00:00
if ( ! self :: box ( 'rename' , $origin , $target )) {
2021-05-10 00:23:30 +00:00
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 ;
}
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Cannot rename "%s" to "%s": ' , $origin , $target ) . self :: $lastError , 0 , null , $target );
2021-05-10 00:23:30 +00:00
}
}
/**
* Tells whether a file exists and is readable .
*
* @ throws IOException When windows path is longer than 258 characters
*/
2021-07-05 23:06:30 +00:00
private function isReadable ( string $filename ) : bool
2021-05-10 00:23:30 +00:00
{
$maxPathLength = \PHP_MAXPATHLEN - 2 ;
if ( \strlen ( $filename ) > $maxPathLength ) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Could not check if file is readable because path length exceeds %d characters.' , $maxPathLength ), 0 , null , $filename );
2021-05-10 00:23:30 +00:00
}
return \is_readable ( $filename );
}
/**
* Creates a symbolic link or copy a directory .
*
* @ throws IOException When symlink fails
2021-07-05 22:50:18 +00:00
* @ param string $originDir
* @ param string $targetDir
* @ param bool $copyOnWindows
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function symlink ( $originDir , $targetDir , $copyOnWindows = \false )
2021-05-10 00:23:30 +00:00
{
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
2021-07-05 22:50:18 +00:00
* @ param string $originFile
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function hardlink ( $originFile , $targetFiles )
2021-05-10 00:23:30 +00:00
{
if ( ! $this -> exists ( $originFile )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\FileNotFoundException ( null , 0 , null , $originFile );
2021-05-10 00:23:30 +00:00
}
if ( ! \is_file ( $originFile )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\FileNotFoundException ( \sprintf ( 'Origin file "%s" is not a file.' , $originFile ));
2021-05-10 00:23:30 +00:00
}
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'
*/
2021-07-05 23:06:30 +00:00
private function linkException ( string $origin , string $target , string $linkType )
2021-05-10 00:23:30 +00:00
{
if ( self :: $lastError ) {
if ( '\\' === \DIRECTORY_SEPARATOR && \false !== \strpos ( self :: $lastError , 'error code(1314)' )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\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 );
2021-05-10 00:23:30 +00:00
}
}
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to create "%s" link from "%s" to "%s": ' , $linkType , $origin , $target ) . self :: $lastError , 0 , null , $target );
2021-05-10 00:23:30 +00:00
}
/**
* 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
*
* @ return string | null
2021-07-05 22:50:18 +00:00
* @ param string $path
* @ param bool $canonicalize
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function readlink ( $path , $canonicalize = \false )
2021-05-10 00:23:30 +00:00
{
if ( ! $canonicalize && ! \is_link ( $path )) {
return null ;
}
if ( $canonicalize ) {
if ( ! $this -> exists ( $path )) {
return null ;
}
2021-06-01 12:39:02 +00:00
if ( '\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70410 ) {
2021-05-10 00:23:30 +00:00
$path = \readlink ( $path );
}
return \realpath ( $path );
}
2021-06-01 12:39:02 +00:00
if ( '\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 70400 ) {
2021-05-10 00:23:30 +00:00
return \realpath ( $path );
}
return \readlink ( $path );
}
/**
* Given an existing path , convert it to a path relative to a given starting path .
*
* @ return string Path of target relative to starting path
2021-07-05 22:50:18 +00:00
* @ param string $endPath
* @ param string $startPath
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function makePathRelative ( $endPath , $startPath )
2021-05-10 00:23:30 +00:00
{
if ( ! $this -> isAbsolutePath ( $startPath )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\InvalidArgumentException ( \sprintf ( 'The start path "%s" is not absolute.' , $startPath ));
2021-05-10 00:23:30 +00:00
}
if ( ! $this -> isAbsolutePath ( $endPath )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\InvalidArgumentException ( \sprintf ( 'The end path "%s" is not absolute.' , $endPath ));
2021-05-10 00:23:30 +00:00
}
// 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
2021-07-05 22:50:18 +00:00
* @ param string $originDir
* @ param string $targetDir
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function mirror ( $originDir , $targetDir , $iterator = null , $options = [])
2021-05-10 00:23:30 +00:00
{
$targetDir = \rtrim ( $targetDir , '/\\' );
$originDir = \rtrim ( $originDir , '/\\' );
$originDirLen = \strlen ( $originDir );
if ( ! $this -> exists ( $originDir )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'The origin directory specified "%s" was not found.' , $originDir ), 0 , null , $originDir );
2021-05-10 00:23:30 +00:00
}
// 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 {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Unable to guess "%s" file type.' , $file ), 0 , null , $file );
2021-05-10 00:23:30 +00:00
}
}
}
/**
* Returns whether the file path is an absolute path .
*
* @ return bool
2021-07-05 22:50:18 +00:00
* @ param string $file
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function isAbsolutePath ( $file )
2021-05-10 00:23:30 +00:00
{
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
2021-07-05 22:50:18 +00:00
* @ param string $dir
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function tempnam ( $dir , $prefix )
2021-05-10 00:23:30 +00:00
{
$suffix = \func_num_args () > 2 ? \func_get_arg ( 2 ) : '' ;
[ $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
2021-06-01 12:39:02 +00:00
if ( $tmpFile = self :: box ( 'tempnam' , $hierarchy , $prefix )) {
2021-05-10 00:23:30 +00:00
if ( null !== $scheme && 'gs' !== $scheme ) {
return $scheme . '://' . $tmpFile ;
}
return $tmpFile ;
}
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( 'A temporary file could not be created: ' . self :: $lastError );
2021-05-10 00:23:30 +00:00
}
// 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
2021-06-01 12:39:02 +00:00
if ( ! ( $handle = self :: box ( 'fopen' , $tmpFile , 'x+' ))) {
2021-05-10 00:23:30 +00:00
continue ;
}
// Close the file if it was successfully opened
2021-06-01 12:39:02 +00:00
self :: box ( 'fclose' , $handle );
2021-05-10 00:23:30 +00:00
return $tmpFile ;
}
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( 'A temporary file could not be created: ' . self :: $lastError );
2021-05-10 00:23:30 +00:00
}
/**
* 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
2021-07-05 22:50:18 +00:00
* @ param string $filename
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function dumpFile ( $filename , $content )
2021-05-10 00:23:30 +00:00
{
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 {
2021-06-01 12:39:02 +00:00
if ( \false === self :: box ( 'file_put_contents' , $tmpFile , $content )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to write file "%s": ' , $filename ) . self :: $lastError , 0 , null , $filename );
2021-05-10 00:23:30 +00:00
}
2021-06-01 12:39:02 +00:00
self :: box ( 'chmod' , $tmpFile , \file_exists ( $filename ) ? \fileperms ( $filename ) : 0666 & ~ \umask ());
2021-05-10 00:23:30 +00:00
$this -> rename ( $tmpFile , $filename , \true );
} finally {
if ( \file_exists ( $tmpFile )) {
2021-06-01 12:39:02 +00:00
self :: box ( 'unlink' , $tmpFile );
2021-05-10 00:23:30 +00:00
}
}
}
/**
* Appends content to an existing file .
*
* @ param string | resource $content The content to append
*
* @ throws IOException If the file is not writable
2021-07-05 22:50:18 +00:00
* @ param string $filename
2021-05-10 00:23:30 +00:00
*/
2021-07-05 22:50:18 +00:00
public function appendToFile ( $filename , $content )
2021-05-10 00:23:30 +00:00
{
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 );
}
2021-06-01 12:39:02 +00:00
if ( \false === self :: box ( 'file_put_contents' , $filename , $content , \FILE_APPEND )) {
2021-07-06 08:19:07 +00:00
throw new \RectorPrefix20210706\Symfony\Component\Filesystem\Exception\IOException ( \sprintf ( 'Failed to write file "%s": ' , $filename ) . self :: $lastError , 0 , null , $filename );
2021-05-10 00:23:30 +00:00
}
}
private function toIterable ( $files ) : iterable
{
return \is_array ( $files ) || $files instanceof \Traversable ? $files : [ $files ];
}
/**
* Gets a 2 - tuple of scheme ( may be null ) and hierarchical part of a filename ( e . g . file :/// tmp -> [ file , tmp ]) .
*/
2021-07-05 23:06:30 +00:00
private function getSchemeAndHierarchy ( string $filename ) : array
2021-05-10 00:23:30 +00:00
{
$components = \explode ( '://' , $filename , 2 );
return 2 === \count ( $components ) ? [ $components [ 0 ], $components [ 1 ]] : [ null , $components [ 0 ]];
}
/**
* @ param mixed ... $args
*
* @ return mixed
*/
2021-07-05 23:06:30 +00:00
private static function box ( callable $func , ... $args )
2021-05-10 00:23:30 +00:00
{
self :: $lastError = null ;
\set_error_handler ( __CLASS__ . '::handleError' );
try {
$result = $func ( ... $args );
\restore_error_handler ();
return $result ;
} catch ( \Throwable $e ) {
}
\restore_error_handler ();
throw $e ;
}
/**
* @ internal
*/
public static function handleError ( $type , $msg )
{
self :: $lastError = $msg ;
}
}