[Filesystem] Add feature to create hardlinks for files
This commit is contained in:
parent
030abb25dc
commit
8475002dfb
@ -330,14 +330,57 @@ class Filesystem
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!$ok && true !== @symlink($originDir, $targetDir)) {
|
if (!$ok && true !== @symlink($originDir, $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();
|
$report = error_get_last();
|
||||||
if (is_array($report)) {
|
if (is_array($report)) {
|
||||||
if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) {
|
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('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 symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir);
|
throw new IOException(sprintf('Failed to create %s link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -565,6 +565,20 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
$this->filesystem->chown($link, $this->getFileOwner($link));
|
$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
|
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
|
||||||
*/
|
*/
|
||||||
@ -582,6 +596,23 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
$this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999));
|
$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
|
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
|
||||||
*/
|
*/
|
||||||
@ -631,6 +662,20 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
$this->filesystem->chgrp($link, $this->getFileGroup($link));
|
$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
|
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
|
||||||
*/
|
*/
|
||||||
@ -648,6 +693,23 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
$this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999));
|
$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
|
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
|
||||||
*/
|
*/
|
||||||
@ -799,6 +861,103 @@ class FilesystemTest extends FilesystemTestCase
|
|||||||
$this->assertEquals($file, readlink($link2));
|
$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
|
* @dataProvider providePathsForMakePathRelative
|
||||||
*/
|
*/
|
||||||
|
@ -29,16 +29,42 @@ class FilesystemTestCase extends \PHPUnit_Framework_TestCase
|
|||||||
*/
|
*/
|
||||||
protected $workspace = null;
|
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;
|
private static $symlinkOnWindows = null;
|
||||||
|
|
||||||
public static function setUpBeforeClass()
|
public static function setUpBeforeClass()
|
||||||
{
|
{
|
||||||
if ('\\' === DIRECTORY_SEPARATOR && null === self::$symlinkOnWindows) {
|
if ('\\' === DIRECTORY_SEPARATOR) {
|
||||||
$target = tempnam(sys_get_temp_dir(), 'sl');
|
self::$linkOnWindows = true;
|
||||||
$link = sys_get_temp_dir().'/sl'.microtime(true).mt_rand();
|
$originFile = tempnam(sys_get_temp_dir(), 'li');
|
||||||
self::$symlinkOnWindows = @symlink($target, $link) && is_link($link);
|
$targetFile = tempnam(sys_get_temp_dir(), 'li');
|
||||||
@unlink($link);
|
if (true !== @link($originFile, $targetFile)) {
|
||||||
unlink($target);
|
$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');
|
$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)
|
protected function markAsSkippedIfSymlinkIsMissing($relative = false)
|
||||||
{
|
{
|
||||||
if ('\\' === DIRECTORY_SEPARATOR && false === self::$symlinkOnWindows) {
|
if ('\\' === DIRECTORY_SEPARATOR && false === self::$symlinkOnWindows) {
|
||||||
|
Reference in New Issue
Block a user