[Filesystem] Add feature to create hardlinks for files

This commit is contained in:
André R 2015-08-04 13:40:34 +02:00 committed by Fabien Potencier
parent 030abb25dc
commit 8475002dfb
3 changed files with 252 additions and 13 deletions

View File

@ -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.
*

View File

@ -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
*/

View File

@ -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) {