diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 95c3ab0392..92c296bc00 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -330,16 +330,59 @@ class Filesystem } if (!$ok && true !== @symlink($originDir, $targetDir)) { - $report = error_get_last(); - if (is_array($report)) { - if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) { - throw new IOException('Unable to create symlink due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', 0, null, $targetDir); - } - } - throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir); + $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)); + } + + foreach ($this->toIterator($targetFiles) as $targetFile) { + if (is_file($targetFile)) { + if (fileinode($originFile) === fileinode($targetFile)) { + continue; + } + $this->remove($targetFile); + } + + if (true !== @link($originFile, $targetFile)) { + $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) + { + $report = error_get_last(); + if (is_array($report)) { + if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) { + throw new IOException(sprintf('Unable to create %s link due to error code 1314: \'A required privilege is not held by the client\'. Do you have the required Administrator-rights?', $linkType), 0, null, $target); + } + } + throw new IOException(sprintf('Failed to create %s link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target); + } + /** * Given an existing path, convert it to a path relative to a given starting path. * diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index 9a65543098..1934a33b1e 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -565,6 +565,20 @@ class FilesystemTest extends FilesystemTestCase $this->filesystem->chown($link, $this->getFileOwner($link)); } + public function testChownLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $this->filesystem->chown($link, $this->getFileOwner($link)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -582,6 +596,23 @@ class FilesystemTest extends FilesystemTestCase $this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999)); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ + public function testChownLinkFails() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -631,6 +662,20 @@ class FilesystemTest extends FilesystemTestCase $this->filesystem->chgrp($link, $this->getFileGroup($link)); } + public function testChgrpLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $this->filesystem->chgrp($link, $this->getFileGroup($link)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -648,6 +693,23 @@ class FilesystemTest extends FilesystemTestCase $this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999)); } + /** + * @expectedException \Symfony\Component\Filesystem\Exception\IOException + */ + public function testChgrpLinkFails() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->hardlink($file, $link); + + $this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999)); + } + /** * @expectedException \Symfony\Component\Filesystem\Exception\IOException */ @@ -799,6 +861,103 @@ class FilesystemTest extends FilesystemTestCase $this->assertEquals($file, readlink($link2)); } + public function testLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + $this->filesystem->hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + /** + * @depends testLink + */ + public function testRemoveLink() + { + $this->markAsSkippedIfLinkIsMissing(); + + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + $this->filesystem->remove($link); + + $this->assertTrue(!is_file($link)); + } + + public function testLinkIsOverwrittenIfPointsToDifferentTarget() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $file2 = $this->workspace.DIRECTORY_SEPARATOR.'file2'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + touch($file2); + link($file2, $link); + + $this->filesystem->hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + + public function testLinkIsNotOverwrittenIfAlreadyCreated() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + link($file, $link); + + $this->filesystem->hardlink($file, $link); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + + } + + public function testLinkWithSeveralTargets() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link1 = $this->workspace.DIRECTORY_SEPARATOR.'link'; + $link2 = $this->workspace.DIRECTORY_SEPARATOR.'link2'; + + touch($file); + + $this->filesystem->hardlink($file, array($link1,$link2)); + + $this->assertTrue(is_file($link1)); + $this->assertEquals(fileinode($file), fileinode($link1)); + $this->assertTrue(is_file($link2)); + $this->assertEquals(fileinode($file), fileinode($link2)); + } + + public function testLinkWithSameTarget() + { + $this->markAsSkippedIfLinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + // practically same as testLinkIsNotOverwrittenIfAlreadyCreated + $this->filesystem->hardlink($file, array($link,$link)); + + $this->assertTrue(is_file($link)); + $this->assertEquals(fileinode($file), fileinode($link)); + } + /** * @dataProvider providePathsForMakePathRelative */ diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php index 63d8b8fc90..9014ab41bc 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php @@ -29,16 +29,42 @@ class FilesystemTestCase extends \PHPUnit_Framework_TestCase */ protected $workspace = null; + /** + * @var null|bool Flag for hard links on Windows + */ + private static $linkOnWindows = null; + + /** + * @var null|bool Flag for symbolic links on Windows + */ private static $symlinkOnWindows = null; public static function setUpBeforeClass() { - if ('\\' === DIRECTORY_SEPARATOR && null === self::$symlinkOnWindows) { - $target = tempnam(sys_get_temp_dir(), 'sl'); - $link = sys_get_temp_dir().'/sl'.microtime(true).mt_rand(); - self::$symlinkOnWindows = @symlink($target, $link) && is_link($link); - @unlink($link); - unlink($target); + if ('\\' === DIRECTORY_SEPARATOR) { + self::$linkOnWindows = true; + $originFile = tempnam(sys_get_temp_dir(), 'li'); + $targetFile = tempnam(sys_get_temp_dir(), 'li'); + if (true !== @link($originFile, $targetFile)) { + $report = error_get_last(); + if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) { + self::$linkOnWindows = false; + } + } else { + @unlink($targetFile); + } + + self::$symlinkOnWindows = true; + $originDir = tempnam(sys_get_temp_dir(), 'sl'); + $targetDir = tempnam(sys_get_temp_dir(), 'sl'); + if (true !== @symlink($originDir, $targetDir)) { + $report = error_get_last(); + if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) { + self::$symlinkOnWindows = false; + } + } else { + @unlink($targetDir); + } } } @@ -100,6 +126,17 @@ class FilesystemTestCase extends \PHPUnit_Framework_TestCase $this->markTestSkipped('Unable to retrieve file group name'); } + protected function markAsSkippedIfLinkIsMissing() + { + if (!function_exists('link')) { + $this->markTestSkipped('link is not supported'); + } + + if ('\\' === DIRECTORY_SEPARATOR && false === self::$linkOnWindows) { + $this->markTestSkipped('link requires "Create hard links" privilege on windows'); + } + } + protected function markAsSkippedIfSymlinkIsMissing($relative = false) { if ('\\' === DIRECTORY_SEPARATOR && false === self::$symlinkOnWindows) {