Skip to content

Commit

Permalink
Porting part of Composer's filesystem to get around renaming files ac…
Browse files Browse the repository at this point in the history
…ross drives

See symfony/symfony-installer#46 and symfony/symfony#12533
  • Loading branch information
weaverryan committed Dec 7, 2014
1 parent be37e2a commit 801a00f
Show file tree
Hide file tree
Showing 4 changed files with 439 additions and 2 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"require": {
"php": ">=5.4.0",
"pimple/pimple": "~3.0",
"symfony/filesystem": ">=2.4, <3.0"
"symfony/filesystem": ">=2.4, <3.0",
"symfony/process": ">=2.4, <3.0"
},
"require-dev": {
"raulfraile/ladybug": "~1.0",
Expand Down
4 changes: 3 additions & 1 deletion src/Distill.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use Distill\Strategy\StrategyInterface;
use Distill\Format\FormatInterface;
use Pimple\Container;
use Symfony\Component\Filesystem\Filesystem;
use Distill\Extractor\Util\Filesystem;

class Distill
{
Expand Down Expand Up @@ -164,6 +164,8 @@ public function extractWithoutRootDirectory($file, $path, FormatInterface $forma
{
$this->initializeIfNotInitialized();

// uses the special, internal filesystem due to a issues with Symfony's
// rename across drives. This was adapted from Composer
$filesystem = new Filesystem();

// extract to a temporary place
Expand Down
325 changes: 325 additions & 0 deletions src/Extractor/Util/Filesystem.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
<?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.
*/

namespace Distill\Extractor\Util;

use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

/**
* Taken from Composer: https://github.com/composer/composer/blob/master/src/Composer/Util/Filesystem.php
*/
class Filesystem
{
private $processExecutor;

public function __construct(ProcessExecutor $executor = null)
{
$this->processExecutor = $executor ?: new ProcessExecutor();
}

public function remove($file)
{
if (is_dir($file)) {
return $this->removeDirectory($file);
}

if (file_exists($file)) {
return $this->unlink($file);
}

return false;
}

/**
* Recursively remove a directory
*
* Uses the process component if proc_open is enabled on the PHP
* installation.
*
* @param string $directory
* @return bool
*
* @throws \RuntimeException
*/
public function removeDirectory($directory)
{
if ($this->isSymlinkedDirectory($directory)) {
return $this->unlinkSymlinkedDirectory($directory);
}

if (!file_exists($directory) || !is_dir($directory)) {
return true;
}

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.');
}

if (!function_exists('proc_open')) {
return $this->removeDirectoryPhp($directory);
}

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
$cmd = sprintf('rmdir /S /Q %s', ProcessExecutor::escape(realpath($directory)));
} else {
$cmd = sprintf('rm -rf %s', ProcessExecutor::escape($directory));
}

$result = $this->getProcess()->execute($cmd, $output) === 0;

// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache();

if ($result && !file_exists($directory)) {
return true;
}

return $this->removeDirectoryPhp($directory);
}

/**
* 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.
*
* @param string $directory
* @return bool
*/
public function removeDirectoryPhp($directory)
{
$it = new RecursiveDirectoryIterator($directory, RecursiveDirectoryIterator::SKIP_DOTS);
$ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::CHILD_FIRST);

foreach ($ri as $file) {
if ($file->isDir()) {
$this->rmdir($file->getPathname());
} else {
$this->unlink($file->getPathname());
}
}

return $this->rmdir($directory);
}

public function ensureDirectoryExists($directory)
{
if (!is_dir($directory)) {
if (file_exists($directory)) {
throw new \RuntimeException(
$directory.' exists and is not a directory.'
);
}
if (!@mkdir($directory, 0777, true)) {
throw new \RuntimeException(
$directory.' does not exist and could not be created.'
);
}
}
}

/**
* Attempts to unlink a file and in case of failure retries after 350ms on windows
*
* @param string $path
* @return bool
*
* @throws \RuntimeException
*/
public function unlink($path)
{
if (!@$this->unlinkImplementation($path)) {
// retry after a bit on windows since it tends to be touchy with mass removals
if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(350000) && !@$this->unlinkImplementation($path))) {
$error = error_get_last();
$message = 'Could not delete '.$path.': ' . @$error['message'];
if (defined('PHP_WINDOWS_VERSION_BUILD')) {
$message .= "\nThis 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 350ms on windows
*
* @param string $path
* @return bool
*
* @throws \RuntimeException
*/
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 .= "\nThis can be due to an antivirus or the Windows Search Indexer locking the file while they are analyzed";
}

throw new \RuntimeException($message);
}
}

return true;
}

/**
* Copy then delete is a non-atomic version of {@link rename}.
*
* Some systems can't rename and also don't have proc_open,
* which requires this solution.
*
* @param string $source
* @param string $target
*/
public function copyThenRemove($source, $target)
{
if (!is_dir($source)) {
copy($source, $target);
$this->unlink($source);

return;
}

$it = new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS);
$ri = new RecursiveIteratorIterator($it, RecursiveIteratorIterator::SELF_FIRST);
$this->ensureDirectoryExists($target);

foreach ($ri as $file) {
$targetPath = $target . DIRECTORY_SEPARATOR . $ri->getSubPathName();
if ($file->isDir()) {
$this->ensureDirectoryExists($targetPath);
} else {
copy($file->getPathname(), $targetPath);
}
}

$this->removeDirectoryPhp($source);
}

public function rename($source, $target)
{
if (true === @rename($source, $target)) {
return;
}

if (!function_exists('proc_open')) {
return $this->copyThenRemove($source, $target);
}

if (defined('PHP_WINDOWS_VERSION_BUILD')) {
// Try to copy & delete - this is a workaround for random "Access denied" errors.
$command = sprintf('xcopy %s %s /E /I /Q', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
$result = $this->processExecutor->execute($command, $output);

// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache();

if (0 === $result) {
$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', ProcessExecutor::escape($source), ProcessExecutor::escape($target));
$result = $this->processExecutor->execute($command, $output);

// clear stat cache because external processes aren't tracked by the php stat cache
clearstatcache();

if (0 === $result) {
return;
}
}

return $this->copyThenRemove($source, $target);
}

protected function getProcess()
{
return new ProcessExecutor;
}

/**
* delete symbolic link implementation (commonly known as "unlink()")
*
* symbolic links on windows which link to directories need rmdir instead of unlink
*
* @param string $path
*
* @return bool
*/
private function unlinkImplementation($path)
{
if (defined('PHP_WINDOWS_VERSION_BUILD') && is_dir($path) && is_link($path)) {
return rmdir($path);
}

return unlink($path);
}

private function isSymlinkedDirectory($directory)
{
if (!is_dir($directory)) {
return false;
}

$resolved = $this->resolveSymlinkedDirectorySymlink($directory);

return is_link($resolved);
}

/**
* @param string $directory
*
* @return bool
*/
private function unlinkSymlinkedDirectory($directory)
{
$resolved = $this->resolveSymlinkedDirectorySymlink($directory);

return $this->unlink($resolved);
}

/**
* resolve pathname to symbolic link of a directory
*
* @param string $pathname directory path to resolve
*
* @return string resolved path to symbolic link or original pathname (unresolved)
*/
private function resolveSymlinkedDirectorySymlink($pathname)
{
if (!is_dir($pathname)) {
return $pathname;
}

$resolved = rtrim($pathname, '/');

if (!strlen($resolved)) {
return $pathname;
}

return $resolved;
}
}
Loading

0 comments on commit 801a00f

Please sign in to comment.