2011-03-26 07:37:25 +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 .
*/
2011-12-22 18:36:46 +00:00
namespace Symfony\Component\Filesystem ;
2011-03-26 07:37:25 +00:00
2013-09-27 15:40:55 +01:00
use Symfony\Component\Filesystem\Exception\FileNotFoundException ;
2017-09-28 17:40:37 +01:00
use Symfony\Component\Filesystem\Exception\InvalidArgumentException ;
2012-06-18 11:41:52 +01:00
use Symfony\Component\Filesystem\Exception\IOException ;
2011-03-26 07:37:25 +00:00
/**
* Provides basic utility to manipulate the file system .
*
* @ author Fabien Potencier < fabien @ symfony . com >
*/
class Filesystem
{
2018-05-11 03:25:00 +01:00
private static $lastError ;
2011-03-26 07:37:25 +00:00
/**
* Copies a file .
*
2016-03-16 14:32:55 +00:00
* 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 .
2011-03-26 07:37:25 +00:00
*
2016-03-16 14:32:55 +00:00
* @ param string $originFile The original filename
* @ param string $targetFile The target filename
* @ param bool $overwriteNewerFiles If true , target files newer than origin files are overwritten
2012-05-18 15:19:51 +01:00
*
2014-12-04 20:26:11 +00:00
* @ throws FileNotFoundException When originFile doesn ' t exist
* @ throws IOException When copy fails
2011-03-26 07:37:25 +00:00
*/
2016-03-16 14:32:55 +00:00
public function copy ( $originFile , $targetFile , $overwriteNewerFiles = false )
2011-03-26 07:37:25 +00:00
{
2017-07-05 08:25:28 +01:00
$originIsLocal = stream_is_local ( $originFile ) || 0 === stripos ( $originFile , 'file://' );
if ( $originIsLocal && ! is_file ( $originFile )) {
2013-09-27 15:53:24 +01:00
throw new FileNotFoundException ( sprintf ( 'Failed to copy "%s" because file does not exist.' , $originFile ), 0 , null , $originFile );
2013-04-28 16:32:44 +01:00
}
2018-07-05 12:24:53 +01:00
$this -> mkdir ( \dirname ( $targetFile ));
2011-03-26 07:37:25 +00:00
2015-03-20 16:17:32 +00:00
$doCopy = true ;
2016-03-16 14:32:55 +00:00
if ( ! $overwriteNewerFiles && null === parse_url ( $originFile , PHP_URL_HOST ) && is_file ( $targetFile )) {
2011-08-26 09:34:23 +01:00
$doCopy = filemtime ( $originFile ) > filemtime ( $targetFile );
2011-03-26 07:37:25 +00:00
}
2011-08-26 09:34:23 +01:00
if ( $doCopy ) {
2013-04-28 16:32:44 +01:00
// https://bugs.php.net/bug.php?id=64634
2014-05-21 19:12:51 +01:00
if ( false === $source = @ fopen ( $originFile , 'r' )) {
throw new IOException ( sprintf ( 'Failed to copy "%s" to "%s" because source file could not be opened for reading.' , $originFile , $targetFile ), 0 , null , $originFile );
}
2014-09-03 10:12:11 +01:00
2014-08-26 12:48:14 +01:00
// Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default
2019-01-16 18:24:45 +00:00
if ( false === $target = @ fopen ( $targetFile , 'w' , null , stream_context_create ([ 'ftp' => [ 'overwrite' => true ]]))) {
2014-05-21 19:12:51 +01:00
throw new IOException ( sprintf ( 'Failed to copy "%s" to "%s" because target file could not be opened for writing.' , $originFile , $targetFile ), 0 , null , $originFile );
}
2014-09-03 10:12:11 +01:00
2014-09-05 10:25:44 +01:00
$bytesCopied = stream_copy_to_stream ( $source , $target );
2013-04-28 16:32:44 +01:00
fclose ( $source );
fclose ( $target );
unset ( $source , $target );
if ( ! is_file ( $targetFile )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to copy "%s" to "%s".' , $originFile , $targetFile ), 0 , null , $originFile );
2012-05-18 15:19:51 +01:00
}
2014-09-05 10:25:44 +01:00
2017-07-05 08:25:28 +01:00
if ( $originIsLocal ) {
// Like `cp`, preserve executable permission bits
@ chmod ( $targetFile , fileperms ( $targetFile ) | ( fileperms ( $originFile ) & 0111 ));
2014-11-29 10:51:28 +00:00
2017-07-05 08:25:28 +01:00
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 );
}
2014-09-05 10:25:44 +01:00
}
2011-03-26 07:37:25 +00:00
}
}
/**
* Creates a directory recursively .
*
2017-11-14 18:49:30 +00:00
* @ param string | iterable $dirs The directory path
* @ param int $mode The directory mode
2011-03-26 07:37:25 +00:00
*
2012-06-18 11:41:52 +01:00
* @ throws IOException On any directory creation failure
2011-03-26 07:37:25 +00:00
*/
public function mkdir ( $dirs , $mode = 0777 )
{
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $dirs ) as $dir ) {
2011-03-26 07:37:25 +00:00
if ( is_dir ( $dir )) {
continue ;
}
2018-05-11 03:25:00 +01:00
if ( ! self :: box ( 'mkdir' , $dir , $mode , true )) {
2014-08-19 09:20:36 +01:00
if ( ! is_dir ( $dir )) {
// The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one
2018-05-11 03:25:00 +01:00
if ( self :: $lastError ) {
throw new IOException ( sprintf ( 'Failed to create "%s": %s.' , $dir , self :: $lastError ), 0 , null , $dir );
2014-08-19 09:20:36 +01:00
}
2014-08-31 04:18:18 +01:00
throw new IOException ( sprintf ( 'Failed to create "%s"' , $dir ), 0 , null , $dir );
2014-08-19 09:20:36 +01:00
}
2012-05-18 15:19:51 +01:00
}
2011-03-26 07:37:25 +00:00
}
}
2012-06-15 17:09:23 +01:00
/**
* Checks the existence of files or directories .
*
2017-11-14 18:49:30 +00:00
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to check
2012-06-15 17:09:23 +01:00
*
2014-11-30 13:33:44 +00:00
* @ return bool true if the file exists , false otherwise
2012-06-15 17:09:23 +01:00
*/
public function exists ( $files )
{
2017-09-26 03:03:27 +01:00
$maxPathLength = PHP_MAXPATHLEN - 2 ;
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $files ) as $file ) {
2018-07-05 12:24:53 +01:00
if ( \strlen ( $file ) > $maxPathLength ) {
2017-09-26 03:03:27 +01:00
throw new IOException ( sprintf ( 'Could not check if file exist because path length exceeds %d characters.' , $maxPathLength ), 0 , null , $file );
2015-12-12 16:45:35 +00:00
}
2012-06-15 17:09:23 +01:00
if ( ! file_exists ( $file )) {
return false ;
}
}
return true ;
}
2011-03-26 07:37:25 +00:00
/**
2012-05-18 15:19:51 +01:00
* Sets access and modification time of file .
2011-03-26 07:37:25 +00:00
*
2017-11-14 18:49:30 +00:00
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to create
2019-02-02 14:27:05 +00:00
* @ 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
2012-05-18 15:19:51 +01:00
*
2012-06-18 11:41:52 +01:00
* @ throws IOException When touch fails
2011-03-26 07:37:25 +00:00
*/
2012-05-18 15:19:51 +01:00
public function touch ( $files , $time = null , $atime = null )
2011-03-26 07:37:25 +00:00
{
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $files ) as $file ) {
2013-01-04 11:28:25 +00:00
$touch = $time ? @ touch ( $file , $time , $atime ) : @ touch ( $file );
if ( true !== $touch ) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to touch "%s".' , $file ), 0 , null , $file );
2012-05-18 15:19:51 +01:00
}
2011-03-26 07:37:25 +00:00
}
}
/**
* Removes files or directories .
*
2017-11-14 18:49:30 +00:00
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to remove
2012-05-18 15:19:51 +01:00
*
2012-06-18 11:41:52 +01:00
* @ throws IOException When removal fails
2011-03-26 07:37:25 +00:00
*/
public function remove ( $files )
{
2016-04-12 09:59:55 +01:00
if ( $files instanceof \Traversable ) {
$files = iterator_to_array ( $files , false );
2018-07-05 12:24:53 +01:00
} elseif ( ! \is_array ( $files )) {
2019-01-16 18:24:45 +00:00
$files = [ $files ];
2016-04-12 09:59:55 +01:00
}
2011-03-26 07:37:25 +00:00
$files = array_reverse ( $files );
foreach ( $files as $file ) {
2016-03-02 13:20:42 +00:00
if ( is_link ( $file )) {
2016-03-08 07:38:51 +00:00
// See https://bugs.php.net/52176
2018-07-26 12:13:39 +01:00
if ( ! ( self :: box ( 'unlink' , $file ) || '\\' !== \DIRECTORY_SEPARATOR || self :: box ( 'rmdir' , $file )) && file_exists ( $file )) {
2018-05-11 03:25:00 +01:00
throw new IOException ( sprintf ( 'Failed to remove symlink "%s": %s.' , $file , self :: $lastError ));
2016-04-12 09:59:55 +01:00
}
2016-03-02 13:20:42 +00:00
} elseif ( is_dir ( $file )) {
2016-04-12 09:59:55 +01:00
$this -> remove ( new \FilesystemIterator ( $file , \FilesystemIterator :: CURRENT_AS_PATHNAME | \FilesystemIterator :: SKIP_DOTS ));
2011-03-26 07:37:25 +00:00
2018-05-11 03:25:00 +01:00
if ( ! self :: box ( 'rmdir' , $file ) && file_exists ( $file )) {
throw new IOException ( sprintf ( 'Failed to remove directory "%s": %s.' , $file , self :: $lastError ));
2012-05-18 15:19:51 +01:00
}
2018-05-11 03:25:00 +01:00
} elseif ( ! self :: box ( 'unlink' , $file ) && file_exists ( $file )) {
throw new IOException ( sprintf ( 'Failed to remove file "%s": %s.' , $file , self :: $lastError ));
2011-03-26 07:37:25 +00:00
}
}
}
/**
* Change mode for an array of files or directories .
*
2017-11-14 18:49:30 +00:00
* @ 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
2012-05-18 15:19:51 +01:00
*
2019-02-02 14:27:05 +00:00
* @ throws IOException When the change fails
2011-03-26 07:37:25 +00:00
*/
2012-05-18 15:19:51 +01:00
public function chmod ( $files , $mode , $umask = 0000 , $recursive = false )
2011-03-26 07:37:25 +00:00
{
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $files ) as $file ) {
2012-05-18 15:19:51 +01:00
if ( true !== @ chmod ( $file , $mode & ~ $umask )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to chmod file "%s".' , $file ), 0 , null , $file );
2012-05-18 15:19:51 +01:00
}
2015-12-02 09:12:52 +00:00
if ( $recursive && is_dir ( $file ) && ! is_link ( $file )) {
$this -> chmod ( new \FilesystemIterator ( $file ), $mode , $umask , true );
}
2012-05-18 15:19:51 +01:00
}
}
/**
2014-12-21 17:00:50 +00:00
* Change the owner of an array of files or directories .
2012-05-18 15:19:51 +01:00
*
2017-11-14 18:49:30 +00:00
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to change owner
* @ param string $user The new owner user name
* @ param bool $recursive Whether change the owner recursively or not
2012-05-18 15:19:51 +01:00
*
2019-02-02 14:27:05 +00:00
* @ throws IOException When the change fails
2012-05-18 15:19:51 +01:00
*/
public function chown ( $files , $user , $recursive = false )
{
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $files ) as $file ) {
2012-05-18 15:19:51 +01:00
if ( $recursive && is_dir ( $file ) && ! is_link ( $file )) {
$this -> chown ( new \FilesystemIterator ( $file ), $user , true );
}
2018-07-05 12:24:53 +01:00
if ( is_link ( $file ) && \function_exists ( 'lchown' )) {
2012-05-18 15:19:51 +01:00
if ( true !== @ lchown ( $file , $user )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to chown file "%s".' , $file ), 0 , null , $file );
2012-05-18 15:19:51 +01:00
}
} else {
if ( true !== @ chown ( $file , $user )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to chown file "%s".' , $file ), 0 , null , $file );
2012-05-18 15:19:51 +01:00
}
}
}
}
/**
2014-12-21 17:00:50 +00:00
* Change the group of an array of files or directories .
2012-05-18 15:19:51 +01:00
*
2017-11-14 18:49:30 +00:00
* @ param string | iterable $files A filename , an array of files , or a \Traversable instance to change group
* @ param string $group The group name
* @ param bool $recursive Whether change the group recursively or not
2012-05-18 15:19:51 +01:00
*
2019-02-02 14:27:05 +00:00
* @ throws IOException When the change fails
2012-05-18 15:19:51 +01:00
*/
public function chgrp ( $files , $group , $recursive = false )
{
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $files ) as $file ) {
2012-05-18 15:19:51 +01:00
if ( $recursive && is_dir ( $file ) && ! is_link ( $file )) {
$this -> chgrp ( new \FilesystemIterator ( $file ), $group , true );
}
2018-07-05 12:24:53 +01:00
if ( is_link ( $file ) && \function_exists ( 'lchgrp' )) {
2017-05-18 18:40:51 +01:00
if ( true !== @ lchgrp ( $file , $group )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to chgrp file "%s".' , $file ), 0 , null , $file );
2012-05-18 15:19:51 +01:00
}
} else {
if ( true !== @ chgrp ( $file , $group )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to chgrp file "%s".' , $file ), 0 , null , $file );
2012-05-18 15:19:51 +01:00
}
}
2011-03-26 07:37:25 +00:00
}
}
/**
2013-06-03 13:55:30 +01:00
* Renames a file or a directory .
2011-03-26 07:37:25 +00:00
*
2014-11-30 13:33:44 +00:00
* @ param string $origin The origin filename or directory
* @ param string $target The new filename or directory
* @ param bool $overwrite Whether to overwrite the target if it already exists
2011-03-26 07:37:25 +00:00
*
2013-06-03 13:55:30 +01:00
* @ throws IOException When target file or directory already exists
2012-06-18 11:41:52 +01:00
* @ throws IOException When origin cannot be renamed
2011-03-26 07:37:25 +00:00
*/
2013-04-23 21:32:37 +01:00
public function rename ( $origin , $target , $overwrite = false )
2011-03-26 07:37:25 +00:00
{
// we check that target does not exist
2015-12-12 16:45:35 +00:00
if ( ! $overwrite && $this -> isReadable ( $target )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Cannot rename because the target "%s" already exists.' , $target ), 0 , null , $target );
2011-03-26 07:37:25 +00:00
}
2012-05-18 15:19:51 +01:00
if ( true !== @ rename ( $origin , $target )) {
2017-06-07 12:57:47 +01:00
if ( is_dir ( $origin )) {
// See https://bugs.php.net/bug.php?id=54097 & http://php.net/manual/en/function.rename.php#113943
2019-01-16 18:24:45 +00:00
$this -> mirror ( $origin , $target , null , [ 'override' => $overwrite , 'delete' => $overwrite ]);
2017-06-07 12:57:47 +01:00
$this -> remove ( $origin );
return ;
}
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Cannot rename "%s" to "%s".' , $origin , $target ), 0 , null , $target );
2012-04-09 19:56:50 +01:00
}
2011-03-26 07:37:25 +00:00
}
2015-12-12 16:45:35 +00:00
/**
* Tells whether a file exists and is readable .
*
2016-06-28 06:50:50 +01:00
* @ param string $filename Path to the file
2015-12-12 16:45:35 +00:00
*
2016-09-02 19:13:13 +01:00
* @ return bool
*
2015-12-12 16:45:35 +00:00
* @ throws IOException When windows path is longer than 258 characters
*/
private function isReadable ( $filename )
{
2017-09-26 03:03:27 +01:00
$maxPathLength = PHP_MAXPATHLEN - 2 ;
2018-07-05 12:24:53 +01:00
if ( \strlen ( $filename ) > $maxPathLength ) {
2017-09-26 03:03:27 +01:00
throw new IOException ( sprintf ( 'Could not check if file is readable because path length exceeds %d characters.' , $maxPathLength ), 0 , null , $filename );
2015-12-12 16:45:35 +00:00
}
return is_readable ( $filename );
}
2011-03-26 07:37:25 +00:00
/**
* Creates a symbolic link or copy a directory .
*
2014-11-30 13:33:44 +00:00
* @ param string $originDir The origin directory path
* @ param string $targetDir The symbolic link name
* @ param bool $copyOnWindows Whether to copy files if on Windows
2012-05-18 15:19:51 +01:00
*
2012-06-18 11:41:52 +01:00
* @ throws IOException When symlink fails
2011-03-26 07:37:25 +00:00
*/
public function symlink ( $originDir , $targetDir , $copyOnWindows = false )
{
2018-07-26 12:13:39 +01:00
if ( '\\' === \DIRECTORY_SEPARATOR ) {
2016-01-30 17:41:36 +00:00
$originDir = strtr ( $originDir , '/' , '\\' );
$targetDir = strtr ( $targetDir , '/' , '\\' );
2014-12-05 20:11:30 +00:00
2016-01-30 17:41:36 +00:00
if ( $copyOnWindows ) {
$this -> mirror ( $originDir , $targetDir );
return ;
}
2011-03-26 07:37:25 +00:00
}
2018-07-05 12:24:53 +01:00
$this -> mkdir ( \dirname ( $targetDir ));
2012-04-19 18:29:30 +01:00
2011-03-26 07:37:25 +00:00
if ( is_link ( $targetDir )) {
2018-05-11 03:25:00 +01:00
if ( readlink ( $targetDir ) === $originDir ) {
return ;
2011-03-26 07:37:25 +00:00
}
2018-05-11 03:25:00 +01:00
$this -> remove ( $targetDir );
2011-03-26 07:37:25 +00:00
}
2018-05-11 03:25:00 +01:00
if ( ! self :: box ( 'symlink' , $originDir , $targetDir )) {
2015-08-04 12:40:34 +01:00
$this -> linkException ( $originDir , $targetDir , 'symbolic' );
}
}
/**
* Creates a hard link , or several hard links to a file .
*
* @ param string $originFile The original 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 ( $originFile , $targetFiles )
{
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 ));
}
2017-11-11 20:57:50 +00:00
foreach ( $this -> toIterable ( $targetFiles ) as $targetFile ) {
2015-08-04 12:40:34 +01:00
if ( is_file ( $targetFile )) {
if ( fileinode ( $originFile ) === fileinode ( $targetFile )) {
continue ;
2012-06-18 09:34:56 +01:00
}
2015-08-04 12:40:34 +01:00
$this -> remove ( $targetFile );
}
2018-05-16 09:49:21 +01:00
if ( ! self :: box ( 'link' , $originFile , $targetFile )) {
2015-08-04 12:40:34 +01:00
$this -> linkException ( $originFile , $targetFile , 'hard' );
}
}
}
/**
* @ param string $origin
* @ param string $target
* @ param string $linkType Name of the link type , typically 'symbolic' or 'hard'
*/
private function linkException ( $origin , $target , $linkType )
{
2018-05-16 09:49:21 +01:00
if ( self :: $lastError ) {
2018-07-26 12:19:56 +01:00
if ( '\\' === \DIRECTORY_SEPARATOR && false !== strpos ( self :: $lastError , 'error code(1314)' )) {
2015-08-04 12:40:34 +01:00
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 );
2012-05-18 15:19:51 +01:00
}
2011-03-26 07:37:25 +00:00
}
2015-08-04 12:40:34 +01:00
throw new IOException ( sprintf ( 'Failed to create %s link from "%s" to "%s".' , $linkType , $origin , $target ), 0 , null , $target );
2011-03-26 07:37:25 +00:00
}
2016-01-22 16:47:47 +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
*
* @ param string $path A filesystem path
* @ param bool $canonicalize Whether or not to return a canonicalized path
*
* @ return string | null
*/
public function readlink ( $path , $canonicalize = false )
{
if ( ! $canonicalize && ! is_link ( $path )) {
return ;
}
if ( $canonicalize ) {
if ( ! $this -> exists ( $path )) {
return ;
}
2018-07-26 12:19:56 +01:00
if ( '\\' === \DIRECTORY_SEPARATOR ) {
2016-01-22 16:47:47 +00:00
$path = readlink ( $path );
}
return realpath ( $path );
}
2018-07-26 12:19:56 +01:00
if ( '\\' === \DIRECTORY_SEPARATOR ) {
2016-01-22 16:47:47 +00:00
return realpath ( $path );
}
return readlink ( $path );
}
2011-09-28 16:31:08 +01:00
/**
2014-12-21 17:00:50 +00:00
* Given an existing path , convert it to a path relative to a given starting path .
2011-09-28 16:31:08 +01:00
*
2012-04-07 00:05:37 +01:00
* @ param string $endPath Absolute path of target
* @ param string $startPath Absolute path where traversal begins
2011-09-28 16:31:08 +01:00
*
* @ return string Path of target relative to starting path
*/
public function makePathRelative ( $endPath , $startPath )
{
2017-09-28 17:40:37 +01:00
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 ));
2017-09-14 13:29:42 +01:00
}
2013-12-27 15:08:19 +00:00
// Normalize separators on Windows
2018-07-26 12:13:39 +01:00
if ( '\\' === \DIRECTORY_SEPARATOR ) {
2015-08-25 13:59:33 +01:00
$endPath = str_replace ( '\\' , '/' , $endPath );
$startPath = str_replace ( '\\' , '/' , $startPath );
2012-04-19 11:37:15 +01:00
}
2017-04-06 19:04:32 +01:00
$stripDriveLetter = function ( $path ) {
2018-07-05 12:24:53 +01:00
if ( \strlen ( $path ) > 2 && ':' === $path [ 1 ] && '/' === $path [ 2 ] && ctype_alpha ( $path [ 0 ])) {
2017-04-06 19:04:32 +01:00
return substr ( $path , 2 );
}
return $path ;
};
$endPath = $stripDriveLetter ( $endPath );
$startPath = $stripDriveLetter ( $startPath );
2012-07-10 23:22:37 +01:00
// Split the paths into arrays
$startPathArr = explode ( '/' , trim ( $startPath , '/' ));
$endPathArr = explode ( '/' , trim ( $endPath , '/' ));
2017-11-18 22:56:10 +00:00
$normalizePathArray = function ( $pathSegments ) {
2019-01-16 18:24:45 +00:00
$result = [];
2017-03-21 08:40:59 +00:00
foreach ( $pathSegments as $segment ) {
2017-11-18 22:56:10 +00:00
if ( '..' === $segment ) {
2017-03-21 08:40:59 +00:00
array_pop ( $result );
2017-04-06 19:04:32 +01:00
} elseif ( '.' !== $segment ) {
2017-03-21 08:40:59 +00:00
$result [] = $segment ;
}
}
return $result ;
};
2017-11-18 22:56:10 +00:00
$startPathArr = $normalizePathArray ( $startPathArr );
$endPathArr = $normalizePathArray ( $endPathArr );
2017-03-21 08:40:59 +00:00
2012-07-10 23:22:37 +01:00
// Find for which directory the common path stops
$index = 0 ;
while ( isset ( $startPathArr [ $index ]) && isset ( $endPathArr [ $index ]) && $startPathArr [ $index ] === $endPathArr [ $index ]) {
2015-03-31 00:07:44 +01:00
++ $index ;
2011-09-28 16:31:08 +01:00
}
// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
2018-07-05 12:24:53 +01:00
if ( 1 === \count ( $startPathArr ) && '' === $startPathArr [ 0 ]) {
2016-07-29 15:40:15 +01:00
$depth = 0 ;
} else {
2018-07-05 12:24:53 +01:00
$depth = \count ( $startPathArr ) - $index ;
2016-07-29 15:40:15 +01:00
}
2011-09-28 16:31:08 +01:00
2017-04-06 19:04:32 +01:00
// Repeated "../" for each level need to reach the common path
$traverser = str_repeat ( '../' , $depth );
2011-09-28 16:31:08 +01:00
2018-07-05 12:24:53 +01:00
$endPathRemainder = implode ( '/' , \array_slice ( $endPathArr , $index ));
2012-07-10 23:22:37 +01:00
2011-09-28 16:31:08 +01:00
// Construct $endPath from traversing to the common path, then to the remaining $endPath
2015-03-20 16:17:32 +00:00
$relativePath = $traverser . ( '' !== $endPathRemainder ? $endPathRemainder . '/' : '' );
2012-07-10 23:22:37 +01:00
2015-03-20 16:17:32 +00:00
return '' === $relativePath ? './' : $relativePath ;
2011-09-28 16:31:08 +01:00
}
2011-03-26 07:37:25 +00:00
/**
* Mirrors a directory to another .
*
2018-02-18 17:35:19 +00:00
* 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 )
*
2019-02-02 14:27:05 +00:00
* @ param string $originDir The origin directory
* @ param string $targetDir The target directory
* @ 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 )
2011-03-26 07:37:25 +00:00
*
2012-06-18 11:41:52 +01:00
* @ throws IOException When file type is unknown
2011-03-26 07:37:25 +00:00
*/
2019-01-16 18:24:45 +00:00
public function mirror ( $originDir , $targetDir , \Traversable $iterator = null , $options = [])
2011-03-26 07:37:25 +00:00
{
2012-11-14 21:58:01 +00:00
$targetDir = rtrim ( $targetDir , '/\\' );
$originDir = rtrim ( $originDir , '/\\' );
2018-07-05 12:24:53 +01:00
$originDirLen = \strlen ( $originDir );
2012-11-14 21:58:01 +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 );
}
2018-07-05 12:24:53 +01:00
$targetDirLen = \strlen ( $targetDir );
2012-11-14 21:58:01 +00:00
foreach ( $deleteIterator as $file ) {
2017-07-11 04:43:03 +01:00
$origin = $originDir . substr ( $file -> getPathname (), $targetDirLen );
2012-11-14 21:58:01 +00:00
if ( ! $this -> exists ( $origin )) {
$this -> remove ( $file );
}
}
}
2018-10-28 18:38:52 +00:00
$copyOnWindows = $options [ 'copy_on_windows' ] ? ? false ;
2011-08-26 09:47:18 +01:00
2011-03-26 07:37:25 +00:00
if ( null === $iterator ) {
2011-08-26 09:47:18 +01:00
$flags = $copyOnWindows ? \FilesystemIterator :: SKIP_DOTS | \FilesystemIterator :: FOLLOW_SYMLINKS : \FilesystemIterator :: SKIP_DOTS ;
$iterator = new \RecursiveIteratorIterator ( new \RecursiveDirectoryIterator ( $originDir , $flags ), \RecursiveIteratorIterator :: SELF_FIRST );
2011-03-26 07:37:25 +00:00
}
2014-11-08 19:35:51 +00:00
if ( $this -> exists ( $originDir )) {
$this -> mkdir ( $targetDir );
}
2011-03-26 07:37:25 +00:00
foreach ( $iterator as $file ) {
2018-05-28 09:59:17 +01:00
if ( false === strpos ( $file -> getPath (), $originDir )) {
throw new IOException ( sprintf ( 'Unable to mirror "%s" directory. If the origin directory is relative, try using "realpath" before calling the mirror method.' , $originDir ), 0 , null , $originDir );
}
2017-07-11 04:43:03 +01:00
$target = $targetDir . substr ( $file -> getPathname (), $originDirLen );
2011-03-26 07:37:25 +00:00
2012-11-19 21:14:30 +00:00
if ( $copyOnWindows ) {
2016-03-08 07:38:51 +00:00
if ( is_file ( $file )) {
2012-11-19 21:14:30 +00:00
$this -> copy ( $file , $target , isset ( $options [ 'override' ]) ? $options [ 'override' ] : false );
2012-12-11 10:40:22 +00:00
} elseif ( is_dir ( $file )) {
2012-11-19 21:14:30 +00:00
$this -> mkdir ( $target );
} else {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Unable to guess "%s" file type.' , $file ), 0 , null , $file );
2012-11-19 21:14:30 +00:00
}
2011-03-26 07:37:25 +00:00
} else {
2012-11-19 21:14:30 +00:00
if ( is_link ( $file )) {
2015-06-26 21:41:07 +01:00
$this -> symlink ( $file -> getLinkTarget (), $target );
2012-12-11 10:40:22 +00:00
} elseif ( is_dir ( $file )) {
2012-11-19 21:14:30 +00:00
$this -> mkdir ( $target );
2012-12-11 10:40:22 +00:00
} elseif ( is_file ( $file )) {
2012-11-19 21:14:30 +00:00
$this -> copy ( $file , $target , isset ( $options [ 'override' ]) ? $options [ 'override' ] : false );
} else {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Unable to guess "%s" file type.' , $file ), 0 , null , $file );
2012-11-19 21:14:30 +00:00
}
2011-03-26 07:37:25 +00:00
}
}
}
2011-05-31 10:04:23 +01:00
/**
* Returns whether the file path is an absolute path .
*
* @ param string $file A file path
*
2014-04-16 11:30:19 +01:00
* @ return bool
2011-05-31 10:04:23 +01:00
*/
public function isAbsolutePath ( $file )
{
2015-12-01 11:58:24 +00:00
return strspn ( $file , '/\\' , 0 , 1 )
2018-07-05 12:24:53 +01:00
|| ( \strlen ( $file ) > 3 && ctype_alpha ( $file [ 0 ])
2017-09-14 13:29:42 +01:00
&& ':' === $file [ 1 ]
2016-01-06 13:34:50 +00:00
&& strspn ( $file , '/\\' , 2 , 1 )
2011-05-31 10:04:23 +01:00
)
2011-12-08 12:44:49 +00:00
|| null !== parse_url ( $file , PHP_URL_SCHEME )
2015-12-01 11:58:24 +00:00
;
2011-05-31 10:04:23 +01:00
}
2015-05-07 19:37:53 +01:00
/**
* Creates a temporary file with support for custom stream wrappers .
*
2016-06-29 06:31:50 +01:00
* @ param string $dir The directory where the temporary filename will be created
* @ param string $prefix The prefix of the generated temporary filename
* Note : Windows uses only the first three characters of prefix
2015-05-07 19:37:53 +01:00
*
2016-06-29 06:31:50 +01:00
* @ return string The new temporary filename ( with path ), or throw an exception on failure
2015-05-07 19:37:53 +01:00
*/
public function tempnam ( $dir , $prefix )
{
list ( $scheme , $hierarchy ) = $this -> getSchemeAndHierarchy ( $dir );
2016-03-27 11:20:16 +01:00
// If no scheme or scheme is "file" or "gs" (Google Cloud) create temp file in local filesystem
2016-03-23 10:17:42 +00:00
if ( null === $scheme || 'file' === $scheme || 'gs' === $scheme ) {
2016-07-20 06:41:28 +01:00
$tmpFile = @ tempnam ( $hierarchy , $prefix );
2015-05-07 19:37:53 +01:00
// If tempnam failed or no scheme return the filename otherwise prepend the scheme
2015-10-06 23:13:52 +01:00
if ( false !== $tmpFile ) {
2016-03-23 10:17:42 +00:00
if ( null !== $scheme && 'gs' !== $scheme ) {
2015-10-08 11:58:45 +01:00
return $scheme . '://' . $tmpFile ;
}
2015-10-06 23:13:52 +01:00
return $tmpFile ;
}
2015-10-08 11:58:45 +01:00
throw new IOException ( 'A temporary file could not be created.' );
2015-05-07 19:37:53 +01:00
}
2015-10-08 11:58:45 +01:00
// Loop until we create a valid temp file or have reached 10 attempts
for ( $i = 0 ; $i < 10 ; ++ $i ) {
2015-05-07 19:37:53 +01:00
// Create a unique filename
$tmpFile = $dir . '/' . $prefix . uniqid ( mt_rand (), true );
// Use fopen instead of file_exists as some streams do not support stat
2015-10-19 09:45:30 +01:00
// Use mode 'x+' to atomically check existence and create to avoid a TOCTOU vulnerability
$handle = @ fopen ( $tmpFile , 'x+' );
2015-05-07 19:37:53 +01:00
// If unsuccessful restart the loop
if ( false === $handle ) {
continue ;
}
// Close the file if it was successfully opened
@ fclose ( $handle );
return $tmpFile ;
}
2015-10-08 11:58:45 +01:00
throw new IOException ( 'A temporary file could not be created.' );
2015-05-07 19:37:53 +01:00
}
2013-04-21 08:24:34 +01:00
/**
* Atomically dumps content into a file .
*
2016-06-29 06:40:00 +01:00
* @ param string $filename The file to be written to
* @ param string $content The data to write into the file
2014-11-30 13:33:44 +00:00
*
2017-09-11 10:28:55 +01:00
* @ throws IOException if the file cannot be written to
2013-04-21 08:24:34 +01:00
*/
2015-03-31 23:10:55 +01:00
public function dumpFile ( $filename , $content )
2013-04-21 08:24:34 +01:00
{
2018-07-05 12:24:53 +01:00
$dir = \dirname ( $filename );
2013-04-21 08:24:34 +01:00
if ( ! is_dir ( $dir )) {
$this -> mkdir ( $dir );
2017-01-08 12:55:49 +00:00
}
if ( ! is_writable ( $dir )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Unable to write to the "%s" directory.' , $dir ), 0 , null , $dir );
2013-04-21 08:24:34 +01:00
}
2015-12-18 11:43:50 +00:00
// Will create a temp file with 0600 access rights
// when the filesystem supports chmod.
2015-05-07 19:37:53 +01:00
$tmpFile = $this -> tempnam ( $dir , basename ( $filename ));
2013-04-21 08:24:34 +01:00
if ( false === @ file_put_contents ( $tmpFile , $content )) {
2013-09-27 15:53:24 +01:00
throw new IOException ( sprintf ( 'Failed to write file "%s".' , $filename ), 0 , null , $filename );
2013-04-21 08:24:34 +01:00
}
2017-03-06 19:30:27 +00:00
@ chmod ( $tmpFile , file_exists ( $filename ) ? fileperms ( $filename ) : 0666 & ~ umask ());
2017-03-01 18:26:18 +00:00
2013-04-23 21:32:37 +01:00
$this -> rename ( $tmpFile , $filename , true );
2013-04-21 08:24:34 +01:00
}
2013-09-27 15:40:55 +01:00
2016-11-23 20:33:32 +00:00
/**
* Appends content to an existing file .
*
* @ param string $filename The file to which to append content
* @ param string $content The content to append
*
* @ throws IOException If the file is not writable
*/
public function appendToFile ( $filename , $content )
{
2018-07-26 09:45:46 +01:00
$dir = \dirname ( $filename );
2016-11-23 20:33:32 +00:00
if ( ! is_dir ( $dir )) {
$this -> mkdir ( $dir );
}
if ( ! is_writable ( $dir )) {
throw new IOException ( sprintf ( 'Unable to write to the "%s" directory.' , $dir ), 0 , null , $dir );
}
if ( false === @ file_put_contents ( $filename , $content , FILE_APPEND )) {
throw new IOException ( sprintf ( 'Failed to write file "%s".' , $filename ), 0 , null , $filename );
}
}
2017-11-13 10:20:53 +00:00
private function toIterable ( $files ) : iterable
2013-09-27 15:40:55 +01:00
{
2019-01-16 18:24:45 +00:00
return \is_array ( $files ) || $files instanceof \Traversable ? $files : [ $files ];
2013-09-27 15:40:55 +01:00
}
2015-05-07 19:37:53 +01:00
/**
2019-01-16 18:56:49 +00:00
* Gets a 2 - tuple of scheme ( may be null ) and hierarchical part of a filename ( e . g . file :/// tmp -> [ file , tmp ]) .
2015-05-07 19:37:53 +01:00
*/
2017-10-28 19:15:32 +01:00
private function getSchemeAndHierarchy ( string $filename ) : array
2015-05-07 19:37:53 +01:00
{
$components = explode ( '://' , $filename , 2 );
2019-01-16 18:24:45 +00:00
return 2 === \count ( $components ) ? [ $components [ 0 ], $components [ 1 ]] : [ null , $components [ 0 ]];
2015-05-07 19:37:53 +01:00
}
2018-05-15 22:17:45 +01:00
2018-05-11 03:25:00 +01:00
private static function box ( $func )
{
self :: $lastError = null ;
2019-06-13 11:57:15 +01:00
set_error_handler ( __CLASS__ . '::handleError' );
2018-05-11 03:25:00 +01:00
try {
2018-09-11 08:26:54 +01:00
$result = $func ( ... \array_slice ( \func_get_args (), 1 ));
2019-06-13 11:57:15 +01:00
restore_error_handler ();
2018-05-11 03:25:00 +01:00
return $result ;
} catch ( \Throwable $e ) {
}
2019-06-13 11:57:15 +01:00
restore_error_handler ();
2018-05-11 03:25:00 +01:00
throw $e ;
}
/**
* @ internal
*/
public static function handleError ( $type , $msg )
{
self :: $lastError = $msg ;
}
2011-03-26 07:37:25 +00:00
}