2011-09-28 22:48:17 +00:00
< ? php
/*
* This file is part of Composer .
*
* ( c ) Nils Adermann < naderman @ naderman . de >
* Jordi Boggiano < j . boggiano @ seld . be >
*
* For the full copyright and license information , please view the LICENSE
* file that was distributed with this source code .
*/
2012-02-09 17:45:28 +00:00
namespace Composer\Util ;
2012-01-03 16:03:17 +00:00
2012-11-19 09:29:32 +00:00
use RecursiveDirectoryIterator ;
use RecursiveIteratorIterator ;
2014-06-09 11:12:42 +00:00
use Symfony\Component\Finder\Finder ;
2012-11-19 09:29:32 +00:00
2011-09-28 22:48:17 +00:00
/**
* @ author Jordi Boggiano < j . boggiano @ seld . be >
2012-07-23 15:30:11 +00:00
* @ author Johannes M . Schmitt < schmittjoh @ gmail . com >
2011-09-28 22:48:17 +00:00
*/
class Filesystem
{
2012-08-10 08:14:02 +00:00
private $processExecutor ;
public function __construct ( ProcessExecutor $executor = null )
{
$this -> processExecutor = $executor ? : new ProcessExecutor ();
}
2012-09-07 18:25:07 +00:00
public function remove ( $file )
{
if ( is_dir ( $file )) {
return $this -> removeDirectory ( $file );
}
if ( file_exists ( $file )) {
2014-06-29 16:49:45 +00:00
return $this -> unlink ( $file );
2012-09-07 18:25:07 +00:00
}
return false ;
}
2013-09-26 09:38:33 +00:00
/**
* Checks if a directory is empty
*
2013-10-11 23:12:02 +00:00
* @ param string $dir
2013-09-26 09:38:33 +00:00
* @ return bool
*/
public function isDirEmpty ( $dir )
{
2014-06-09 11:12:42 +00:00
$finder = Finder :: create ()
-> ignoreVCS ( false )
-> ignoreDotFiles ( false )
-> depth ( 0 )
-> in ( $dir );
2013-09-26 09:38:33 +00:00
2014-06-09 11:12:42 +00:00
return count ( $finder ) === 0 ;
2014-06-04 12:20:36 +00:00
}
public function emptyDirectory ( $dir , $ensureDirectoryExists = true )
{
2014-06-29 10:49:11 +00:00
if ( file_exists ( $dir ) && is_link ( $dir )) {
2014-06-29 16:49:45 +00:00
$this -> unlink ( $dir );
2014-06-29 10:49:11 +00:00
}
2014-06-04 12:20:36 +00:00
if ( $ensureDirectoryExists ) {
$this -> ensureDirectoryExists ( $dir );
}
if ( is_dir ( $dir )) {
2014-06-09 11:12:42 +00:00
$finder = Finder :: create ()
-> ignoreVCS ( false )
-> ignoreDotFiles ( false )
-> depth ( 0 )
-> in ( $dir );
foreach ( $finder as $path ) {
$this -> remove (( string ) $path );
2014-06-04 12:20:36 +00:00
}
}
2013-09-26 09:38:33 +00:00
}
2012-11-19 09:29:32 +00:00
/**
* Recursively remove a directory
*
* Uses the process component if proc_open is enabled on the PHP
* installation .
*
2013-01-05 19:01:58 +00:00
* @ param string $directory
2012-11-19 09:29:32 +00:00
* @ return bool
2014-07-16 13:17:38 +00:00
*
* @ throws \RuntimeException
2012-11-19 09:29:32 +00:00
*/
2011-12-03 14:39:06 +00:00
public function removeDirectory ( $directory )
2011-09-28 22:48:17 +00:00
{
2014-06-29 11:00:57 +00:00
if ( file_exists ( $directory ) && is_link ( $directory )) {
2014-06-29 16:49:45 +00:00
return $this -> unlink ( $directory );
2014-06-29 10:49:11 +00:00
}
2014-06-29 16:49:45 +00:00
if ( ! file_exists ( $directory ) || ! is_dir ( $directory )) {
2012-03-07 23:12:38 +00:00
return true ;
}
2013-11-14 20:21:08 +00:00
if ( preg_match ( '{^(?:[a-z]:)?[/\\\\]+$}i' , $directory )) {
throw new \RuntimeException ( 'Aborting an attempted deletion of ' . $directory . ', this was probably not intended, if it is a real use case please report it.' );
}
2012-11-19 09:29:32 +00:00
if ( ! function_exists ( 'proc_open' )) {
return $this -> removeDirectoryPhp ( $directory );
}
2011-09-28 22:48:17 +00:00
if ( defined ( 'PHP_WINDOWS_VERSION_BUILD' )) {
2012-01-18 07:56:35 +00:00
$cmd = sprintf ( 'rmdir /S /Q %s' , escapeshellarg ( realpath ( $directory )));
2011-09-28 22:48:17 +00:00
} else {
2012-01-18 07:56:35 +00:00
$cmd = sprintf ( 'rm -rf %s' , escapeshellarg ( $directory ));
2011-09-28 22:48:17 +00:00
}
2012-01-18 07:56:35 +00:00
2013-02-11 21:52:06 +00:00
$result = $this -> getProcess () -> execute ( $cmd , $output ) === 0 ;
2012-03-07 23:12:38 +00:00
2014-06-29 16:49:45 +00:00
// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache ();
2012-03-07 23:12:38 +00:00
2014-06-29 16:49:45 +00:00
if ( $result && ! file_exists ( $directory )) {
return true ;
2014-05-12 21:12:57 +00:00
}
return $this -> removeDirectoryPhp ( $directory );
2011-09-28 22:48:17 +00:00
}
2011-12-03 14:39:06 +00:00
2012-11-19 09:29:32 +00:00
/**
* Recursively delete directory using PHP iterators .
*
* Uses a CHILD_FIRST RecursiveIteratorIterator to sort files
* before directories , creating a single non - recursive loop
* to delete files / directories in the correct order .
*
2013-01-05 19:01:58 +00:00
* @ param string $directory
2012-11-19 09:29:32 +00:00
* @ return bool
*/
public function removeDirectoryPhp ( $directory )
{
2012-11-19 10:21:41 +00:00
$it = new RecursiveDirectoryIterator ( $directory , RecursiveDirectoryIterator :: SKIP_DOTS );
2012-11-19 09:29:32 +00:00
$ri = new RecursiveIteratorIterator ( $it , RecursiveIteratorIterator :: CHILD_FIRST );
foreach ( $ri as $file ) {
if ( $file -> isDir ()) {
2014-06-29 16:49:45 +00:00
$this -> rmdir ( $file -> getPathname ());
2012-11-19 09:29:32 +00:00
} else {
2014-06-29 16:49:45 +00:00
$this -> unlink ( $file -> getPathname ());
2012-11-19 09:29:32 +00:00
}
}
2014-06-29 16:49:45 +00:00
return $this -> rmdir ( $directory );
2012-11-19 09:29:32 +00:00
}
2011-12-03 14:39:06 +00:00
public function ensureDirectoryExists ( $directory )
{
if ( ! is_dir ( $directory )) {
if ( file_exists ( $directory )) {
throw new \RuntimeException (
$directory . ' exists and is not a directory.'
);
}
2013-02-20 13:51:15 +00:00
if ( !@ mkdir ( $directory , 0777 , true )) {
2011-12-03 14:39:06 +00:00
throw new \RuntimeException (
$directory . ' does not exist and could not be created.'
);
}
}
}
2011-12-03 19:44:00 +00:00
2014-06-29 16:49:45 +00:00
/**
* Attempts to unlink a file and in case of failure retries after 350 ms on windows
*
* @ param string $path
* @ return bool
2014-07-16 13:17:38 +00:00
*
* @ throws \RuntimeException
2014-06-29 16:49:45 +00:00
*/
public function unlink ( $path )
{
if ( !@ unlink ( $path )) {
// retry after a bit on windows since it tends to be touchy with mass removals
if ( ! defined ( 'PHP_WINDOWS_VERSION_BUILD' ) || ( usleep ( 350000 ) && !@ unlink ( $path ))) {
$error = error_get_last ();
$message = 'Could not delete ' . $path . ': ' . @ $error [ 'message' ];
if ( defined ( 'PHP_WINDOWS_VERSION_BUILD' )) {
$message .= " \n This can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed " ;
}
throw new \RuntimeException ( $message );
}
}
return true ;
}
/**
* Attempts to rmdir a file and in case of failure retries after 350 ms on windows
*
* @ param string $path
* @ return bool
2014-07-16 13:17:38 +00:00
*
* @ throws \RuntimeException
2014-06-29 16:49:45 +00:00
*/
public function rmdir ( $path )
{
if ( !@ rmdir ( $path )) {
// retry after a bit on windows since it tends to be touchy with mass removals
if ( ! defined ( 'PHP_WINDOWS_VERSION_BUILD' ) || ( usleep ( 350000 ) && !@ rmdir ( $path ))) {
$error = error_get_last ();
$message = 'Could not delete ' . $path . ': ' . @ $error [ 'message' ];
if ( defined ( 'PHP_WINDOWS_VERSION_BUILD' )) {
$message .= " \n This can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed " ;
}
throw new \RuntimeException ( $message );
}
}
return true ;
}
2012-11-19 10:21:41 +00:00
/**
* Copy then delete is a non - atomic version of { @ link rename } .
*
2013-01-26 18:43:01 +00:00
* Some systems can 't rename and also don' t have proc_open ,
2012-11-19 10:21:41 +00:00
* which requires this solution .
*
* @ param string $source
* @ param string $target
*/
public function copyThenRemove ( $source , $target )
{
$it = new RecursiveDirectoryIterator ( $source , RecursiveDirectoryIterator :: SKIP_DOTS );
$ri = new RecursiveIteratorIterator ( $it , RecursiveIteratorIterator :: SELF_FIRST );
2013-09-20 03:02:06 +00:00
$this -> ensureDirectoryExists ( $target );
2012-11-19 10:21:41 +00:00
foreach ( $ri as $file ) {
$targetPath = $target . DIRECTORY_SEPARATOR . $ri -> getSubPathName ();
if ( $file -> isDir ()) {
2013-09-20 03:02:06 +00:00
$this -> ensureDirectoryExists ( $targetPath );
2012-11-19 10:21:41 +00:00
} else {
copy ( $file -> getPathname (), $targetPath );
}
}
$this -> removeDirectoryPhp ( $source );
}
2012-07-23 15:30:11 +00:00
public function rename ( $source , $target )
{
2012-09-07 18:25:07 +00:00
if ( true === @ rename ( $source , $target )) {
2012-07-23 15:30:11 +00:00
return ;
}
2012-11-19 12:51:24 +00:00
if ( ! function_exists ( 'proc_open' )) {
return $this -> copyThenRemove ( $source , $target );
}
2012-11-19 10:21:41 +00:00
2012-11-19 12:51:24 +00:00
if ( defined ( 'PHP_WINDOWS_VERSION_BUILD' )) {
2012-09-07 18:25:07 +00:00
// Try to copy & delete - this is a workaround for random "Access denied" errors.
$command = sprintf ( 'xcopy %s %s /E /I /Q' , escapeshellarg ( $source ), escapeshellarg ( $target ));
2013-03-21 14:21:54 +00:00
$result = $this -> processExecutor -> execute ( $command , $output );
// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache ();
if ( 0 === $result ) {
2012-09-07 18:25:07 +00:00
$this -> remove ( $source );
return ;
}
} else {
// We do not use PHP's "rename" function here since it does not support
// the case where $source, and $target are located on different partitions.
$command = sprintf ( 'mv %s %s' , escapeshellarg ( $source ), escapeshellarg ( $target ));
2013-03-21 14:21:54 +00:00
$result = $this -> processExecutor -> execute ( $command , $output );
// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache ();
if ( 0 === $result ) {
2012-08-10 08:14:02 +00:00
return ;
}
2012-07-23 15:30:11 +00:00
}
2012-09-07 18:25:07 +00:00
2013-07-25 16:02:01 +00:00
return $this -> copyThenRemove ( $source , $target );
2012-07-23 15:30:11 +00:00
}
2011-12-03 19:44:00 +00:00
/**
* Returns the shortest path from $from to $to
*
2013-06-13 11:28:24 +00:00
* @ param string $from
* @ param string $to
* @ param bool $directories if true , the source / target are considered to be directories
2013-06-13 00:05:44 +00:00
* @ throws \InvalidArgumentException
2011-12-03 19:44:00 +00:00
* @ return string
*/
2012-03-23 11:49:29 +00:00
public function findShortestPath ( $from , $to , $directories = false )
2011-12-03 19:44:00 +00:00
{
if ( ! $this -> isAbsolutePath ( $from ) || ! $this -> isAbsolutePath ( $to )) {
2012-07-23 19:58:47 +00:00
throw new \InvalidArgumentException ( sprintf ( '$from (%s) and $to (%s) must be absolute paths.' , $from , $to ));
2011-12-03 19:44:00 +00:00
}
2013-03-13 21:13:32 +00:00
$from = lcfirst ( $this -> normalizePath ( $from ));
$to = lcfirst ( $this -> normalizePath ( $to ));
2012-03-10 01:14:40 +00:00
2012-03-23 11:49:29 +00:00
if ( $directories ) {
2012-03-22 09:11:48 +00:00
$from .= '/dummy_file' ;
}
2011-12-03 19:44:00 +00:00
if ( dirname ( $from ) === dirname ( $to )) {
return './' . basename ( $to );
}
2013-09-26 12:34:41 +00:00
$commonPath = $to ;
2013-10-28 10:01:17 +00:00
while ( strpos ( $from . '/' , $commonPath . '/' ) !== 0 && '/' !== $commonPath && ! preg_match ( '{^[a-z]:/?$}i' , $commonPath )) {
$commonPath = strtr ( dirname ( $commonPath ), '\\' , '/' );
2011-12-03 19:44:00 +00:00
}
2013-10-28 10:01:17 +00:00
if ( 0 !== strpos ( $from , $commonPath ) || '/' === $commonPath ) {
2011-12-03 19:44:00 +00:00
return $to ;
}
$commonPath = rtrim ( $commonPath , '/' ) . '/' ;
$sourcePathDepth = substr_count ( substr ( $from , strlen ( $commonPath )), '/' );
$commonPathCode = str_repeat ( '../' , $sourcePathDepth );
2012-05-22 10:07:08 +00:00
2011-12-04 21:40:30 +00:00
return ( $commonPathCode . substr ( $to , strlen ( $commonPath ))) ? : './' ;
2011-12-03 19:44:00 +00:00
}
/**
* Returns PHP code that , when executed in $from , will return the path to $to
*
2013-06-13 11:28:24 +00:00
* @ param string $from
* @ param string $to
* @ param bool $directories if true , the source / target are considered to be directories
2013-06-13 00:05:44 +00:00
* @ throws \InvalidArgumentException
2011-12-03 19:44:00 +00:00
* @ return string
*/
2011-12-03 22:21:10 +00:00
public function findShortestPathCode ( $from , $to , $directories = false )
2011-12-03 19:44:00 +00:00
{
if ( ! $this -> isAbsolutePath ( $from ) || ! $this -> isAbsolutePath ( $to )) {
2012-07-23 19:58:47 +00:00
throw new \InvalidArgumentException ( sprintf ( '$from (%s) and $to (%s) must be absolute paths.' , $from , $to ));
2011-12-03 19:44:00 +00:00
}
2013-03-13 21:13:32 +00:00
$from = lcfirst ( $this -> normalizePath ( $from ));
$to = lcfirst ( $this -> normalizePath ( $to ));
2012-03-10 01:14:40 +00:00
2011-12-03 19:44:00 +00:00
if ( $from === $to ) {
2011-12-03 22:21:10 +00:00
return $directories ? '__DIR__' : '__FILE__' ;
2011-12-03 19:44:00 +00:00
}
2013-09-26 12:34:41 +00:00
$commonPath = $to ;
2013-10-17 09:10:16 +00:00
while ( strpos ( $from . '/' , $commonPath . '/' ) !== 0 && '/' !== $commonPath && ! preg_match ( '{^[a-z]:/?$}i' , $commonPath ) && '.' !== $commonPath ) {
2013-10-28 10:01:17 +00:00
$commonPath = strtr ( dirname ( $commonPath ), '\\' , '/' );
2011-12-03 19:44:00 +00:00
}
2013-10-17 09:10:16 +00:00
if ( 0 !== strpos ( $from , $commonPath ) || '/' === $commonPath || '.' === $commonPath ) {
2011-12-03 19:44:00 +00:00
return var_export ( $to , true );
}
$commonPath = rtrim ( $commonPath , '/' ) . '/' ;
2011-12-04 21:40:30 +00:00
if ( strpos ( $to , $from . '/' ) === 0 ) {
2011-12-03 22:21:10 +00:00
return '__DIR__ . ' . var_export ( substr ( $to , strlen ( $from )), true );
}
$sourcePathDepth = substr_count ( substr ( $from , strlen ( $commonPath )), '/' ) + $directories ;
2011-12-03 19:44:00 +00:00
$commonPathCode = str_repeat ( 'dirname(' , $sourcePathDepth ) . '__DIR__' . str_repeat ( ')' , $sourcePathDepth );
2011-12-04 21:40:30 +00:00
$relTarget = substr ( $to , strlen ( $commonPath ));
2012-05-22 10:07:08 +00:00
2011-12-04 21:40:30 +00:00
return $commonPathCode . ( strlen ( $relTarget ) ? '.' . var_export ( '/' . $relTarget , true ) : '' );
2011-12-03 19:44:00 +00:00
}
/**
* Checks if the given path is absolute
*
2012-06-23 09:58:18 +00:00
* @ param string $path
* @ return bool
2011-12-03 19:44:00 +00:00
*/
public function isAbsolutePath ( $path )
{
return substr ( $path , 0 , 1 ) === '/' || substr ( $path , 1 , 1 ) === ':' ;
}
2012-01-18 07:56:35 +00:00
2012-12-16 19:04:39 +00:00
/**
* Returns size of a file or directory specified by path . If a directory is
* given , it ' s size will be computed recursively .
*
2013-06-13 11:28:24 +00:00
* @ param string $path Path to the file or directory
2013-06-13 00:05:44 +00:00
* @ throws \RuntimeException
2012-12-16 19:04:39 +00:00
* @ return int
*/
public function size ( $path )
{
if ( ! file_exists ( $path )) {
throw new \RuntimeException ( " $path does not exist. " );
}
if ( is_dir ( $path )) {
return $this -> directorySize ( $path );
}
2013-01-05 19:01:58 +00:00
2012-12-16 19:04:39 +00:00
return filesize ( $path );
}
2013-03-13 21:13:32 +00:00
/**
* Normalize a path . This replaces backslashes with slashes , removes ending
* slash and collapses redundant separators and up - level references .
*
2013-06-13 11:28:24 +00:00
* @ param string $path Path to the file or directory
2013-03-13 21:13:32 +00:00
* @ return string
*/
public function normalizePath ( $path )
{
$parts = array ();
$path = strtr ( $path , '\\' , '/' );
$prefix = '' ;
2013-04-05 04:41:14 +00:00
$absolute = false ;
2013-03-13 21:13:32 +00:00
2013-04-05 04:41:14 +00:00
if ( preg_match ( '{^([0-9a-z]+:(?://(?:[a-z]:)?)?)}i' , $path , $match )) {
2013-03-13 21:13:32 +00:00
$prefix = $match [ 1 ];
$path = substr ( $path , strlen ( $prefix ));
}
2013-04-05 04:41:14 +00:00
if ( substr ( $path , 0 , 1 ) === '/' ) {
$absolute = true ;
$path = substr ( $path , 1 );
}
$up = false ;
2013-03-13 21:13:32 +00:00
foreach ( explode ( '/' , $path ) as $chunk ) {
2013-04-05 04:41:14 +00:00
if ( '..' === $chunk && ( $absolute || $up )) {
2013-03-13 21:13:32 +00:00
array_pop ( $parts );
2013-04-05 04:41:14 +00:00
$up = ! ( empty ( $parts ) || '..' === end ( $parts ));
2013-03-13 21:13:32 +00:00
} elseif ( '.' !== $chunk && '' !== $chunk ) {
$parts [] = $chunk ;
2013-04-05 04:41:14 +00:00
$up = '..' !== $chunk ;
2013-03-13 21:13:32 +00:00
}
}
2013-04-05 04:41:14 +00:00
return $prefix . ( $absolute ? '/' : '' ) . implode ( '/' , $parts );
2013-03-13 21:13:32 +00:00
}
2014-05-31 19:36:09 +00:00
/**
* Return if the given path is local
*
2014-06-10 14:02:44 +00:00
* @ param string $path
2014-05-31 19:36:09 +00:00
* @ return bool
*/
public static function isLocalPath ( $path )
{
return ( bool ) preg_match ( '{^(file://|/|[a-z]:[\\\\/]|\.\.[\\\\/]|[a-z0-9_.-]+[\\\\/])}i' , $path );
}
2012-12-16 19:04:39 +00:00
protected function directorySize ( $directory )
{
$it = new RecursiveDirectoryIterator ( $directory , RecursiveDirectoryIterator :: SKIP_DOTS );
$ri = new RecursiveIteratorIterator ( $it , RecursiveIteratorIterator :: CHILD_FIRST );
$size = 0 ;
foreach ( $ri as $file ) {
if ( $file -> isFile ()) {
$size += $file -> getSize ();
}
}
2013-01-05 19:01:58 +00:00
2012-12-16 19:04:39 +00:00
return $size ;
}
2012-01-18 07:56:35 +00:00
protected function getProcess ()
{
return new ProcessExecutor ;
}
2012-03-22 09:11:48 +00:00
}