8000 feature #15458 [Filesystem] Add feature to create hardlinks for files… · symfony/symfony@31678c9 · GitHub
[go: up one dir, main page]

8000
Skip to content

Commit 31678c9

Browse files
committed
feature #15458 [Filesystem] Add feature to create hardlinks for files (andrerom)
This PR was submitted for the 2.8 branch but it was merged into the 3.2-dev branch instead (closes #15458). Discussion ---------- [Filesystem] Add feature to create hardlinks for files | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | | License | MIT | Doc PR | Todo: - [x] Tests - [ ] Doc - [x] Getting someone to test on Windows as exception might differ from symlink functionality ## Why Symlinks are good for directories, but for files hardlinks are sometime a better match when needing to represent same file in several locations. One use case for this feature is for multi tagging of Http Cache when running against FileSystem. Multi tagging is used in FoSHttpCache and supported when running Varnish, but not with Symfony HTTPCache, yet.. With hardlinks combined with meta information containing tags within the file, lookup and reverse lookup is thus easily possible. ## What Introduces method `hardlink()` with similar behavior as `symlink`, difference being: - Allowing several targets to be provided to match use case in 'why' - Like `symlink` removes existing target if not the same, but uses `fileinode` to compare target & source Commits ------- 8475002 [Filesystem] Add feature to create hardlinks for files
2 parents 030abb2 + 8475002 commit 31678c9

File tree

3 files changed

+250
-11
lines changed

3 files changed

+250
-11
lines changed

src/Symfony/Component/Filesystem/Filesystem.php

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,57 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false)
330330
}
331331

332332
if (!$ok && true !== @symlink($originDir, $targetDir)) {
333-
$report = error_get_last();
334-
if (is_array($report)) {
335-
if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) {
336-
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);
333+
$this->linkException($originDir, $targetDir, 'symbolic');
334+
}
335+
}
336+
337+
/**
338+
* Creates a hard link, or several hard links to a file.
339+
*
340+
* @param string $originFile The original file
341+
* @param string|string[] $targetFiles The target file(s)
342+
*
343+
* @throws FileNotFoundException When original file is missing or not a file
344+
* @throws IOException When link fails, including if link already exists
345+
*/
346+
public function hardlink($originFile, $targetFiles)
347+
{
348+
if (!$this->exists($originFile)) {
349+
throw new FileNotFoundException(null, 0, null, $originFile);
350+
}
351+
352+
if (!is_file($originFile)) {
353+
throw new FileNotFoundException(sprintf('Origin file "%s" is not a file', $originFile));
354+
}
355+
356+
foreach ($this->toIterator($targetFiles) as $targetFile) {
357+
if (is_file($targetFile)) {
358+
if (fileinode($originFile) === fileinode($targetFile)) {
359+
continue;
337360
}
361+
$this->remove($targetFile);
362+
}
363+
364+
if (true !== @link($originFile, $targetFile)) {
365+
$this->linkException($originFile, $targetFile, 'hard');
366+
}
367+
}
368+
}
369+
370+
/**
371+
* @param string $origin
372+
* @param string $target
373+
* @param string $linkType Name of the link type, typically 'symbolic' or 'hard'
374+
*/
375+
private function linkException($origin, $target, $linkType)
376+
{
377+
$report = error_get_last();
378+
if (is_array($report)) {
379+
if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) {
380+
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);
338381
}
339-
throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir);
340382
}
383+
throw new IOException(sprintf('Failed to create %s link from "%s" to "%s".', $linkType, $origin, $target), 0, null, $target);
341384
}
342385

343386
/**

src/Symfony/Component/Filesystem/Tests/FilesystemTest.php

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,20 @@ public function testChownSymlink()
565565
$this->filesystem->chown($link, $this->getFileOwner($link));
566566
}
567567

568+
public function testChownLink()
569+
{
570+
$this->markAsSkippedIfLinkIsMissing();
571+
572+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
573+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
574+
575+
touch($file);
576+
577+
$this->filesystem->hardlink($file, $link);
578+
579+
$this->filesystem->chown($link, $this->getFileOwner($link));
580+
}
581+
568582
/**
569583
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
570584
*/
@@ -582,6 +596,23 @@ public function testChownSymlinkFails()
582596
$this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999));
583597
}
584598

599+
/**
600+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
601+
*/
602+
public function testChownLinkFails()
603+
{
604+
$this->markAsSkippedIfLinkIsMissing();
605+
606+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
607+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
608+
609+
touch($file);
610+
611+
$this->filesystem->hardlink($file, $link);
612+
613+
$this->filesystem->chown($link, 'user'.time().mt_rand(1000, 9999));
614+
}
615+
585616
/**
586617
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
587618
*/
@@ -631,6 +662,20 @@ public function testChgrpSymlink()
631662
$this->filesystem->chgrp($link, $this->getFileGroup($link));
632663
}
633664

665+
public function testChgrpLink()
666+
{
667+
$this->markAsSkippedIfLinkIsMissing();
668+
669+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
670+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
671+
672+
touch($file);
673+
674+
$this->filesystem->hardlink($file, $link);
675+
676+
$this->filesystem->chgrp($link, $this->getFileGroup($link));
677+
}
678+
634679
/**
635680
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
636681
*/
@@ -648,6 +693,23 @@ public function testChgrpSymlinkFails()
648693
$this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999));
649694
}
650695

696+
/**
697+
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
698+
*/
699+
public function testChgrpLinkFails()
700+
{
701+
$this->markAsSkippedIfLinkIsMissing();
702+
703+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
704+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
705+
706+
touch($file);
707+
708+
$this->filesystem->hardlink($file, $link);
709+
710+
$this->filesystem->chgrp($link, 'user'.time().mt_rand(1000, 9999));
711+
}
712+
651713
/**
652714
* @expectedException \Symfony\Component\Filesystem\Exception\IOException
653715
*/
@@ -799,6 +861,103 @@ public function testSymlinkCreatesTargetDirectoryIfItDoesNotExist()
799861
$this->assertEquals($file, readlink($link2));
800862
}
801863

864+
public function testLink()
865+
{
866+
$this->markAsSkippedIfLinkIsMissing();
867+
868+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
869+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
870+
871+
touch($file);
872+
$this->filesystem->hardlink($file, $link);
873+
874+
$this->assertTrue(is_file($link));
875+
$this->assertEquals(fileinode($file), fileinode($link));
876+
}
877+
878+
/**
879+
* @depends testLink
880+
*/
881+
public function testRemoveLink()
882+
{
883+
$this->markAsSkippedIfLinkIsMissing();
884+
885+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
886+
887+
$this->filesystem->remove($link);
888+
889+
$this->assertTrue(!is_file($link));
890+
}
891+
892+
public function testLinkIsOverwrittenIfPointsToDifferentTarget()
893+
{
894+
$this->markAsSkippedIfLinkIsMissing();
895+
896+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
897+
$file2 = $this->workspace.DIRECTORY_SEPARATOR.'file2';
898+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
899+
900+
touch($file);
901+
touch($file2);
902+
link($file2, $link);
903+
904+
$this->filesystem->hardlink($file, $link);
905+
906+
$this->assertTrue(is_file($link));
907+
$this->assertEquals(fileinode($file), fileinode($link));
908+
}
909+
910+
public function testLinkIsNotOverwrittenIfAlreadyCreated()
911+
{
912+
$this->markAsSkippedIfLinkIsMissing();
913+
914+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
915+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
916+
917+
touch($file);
918+
link($file, $link);
919+
920+
$this->filesystem->hardlink($file, $link);
921+
922+
$this->assertTrue(is_file($link));
923+
$this->assertEquals(fileinode($file), fileinode($link));
924+
925+
}
926+
927+
public function testLinkWithSeveralTargets()
928+
{
929+
$this->markAsSkippedIfLinkIsMissing();
930+
931+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
932+
$link1 = $this->workspace.DIRECTORY_SEPARATOR.'link';
933+
$link2 = $this->workspace.DIRECTORY_SEPARATOR.'link2';
934+
935+
touch($file);
936+
937+
$this->filesystem->hardlink($file, array($link1,$link2));
938+
939+
$this->assertTrue(is_file($link1));
940+
$this->assertEquals(fileinode($file), fileinode($link1));
941+
$this->assertTrue(is_file($link2));
942+
$this->assertEquals(fileinode($file), fileinode($link2));
943+
}
944+
945+
public function testLinkWithSameTarget()
946+
{
947+
$this->markAsSkippedIfLinkIsMissing();
948+
949+
$file = $this->workspace.DIRECTORY_SEPARATOR.'file';
950+
$link = $this->workspace.DIRECTORY_SEPARATOR.'link';
951+
952+
touch($file);
953+
954+
// practically same as testLinkIsNotOverwrittenIfAlreadyCreated
955+
$this->filesystem->hardlink($file, array($link,$link));
956+
957+
$this->assertTrue(is_file($link));
958+
$this->assertEquals(fileinode($file), fileinode($link));
959+
}
960+
802961
/**
803962
* @dataProvider providePathsForMakePathRelative
804963
*/

src/Symfony/Component/Filesystem/Tests/FilesystemTestCase.php

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,42 @@ class FilesystemTestCase extends \PHPUnit_Framework_TestCase
2929
*/
3030
protected $workspace = null;
3131

32+
/**
33+
* @var null|bool Flag for hard links on Windows
34+
*/
35+
private static $linkOnWindows = null;
36+
37+
/**
38+
* @var null|bool Flag for symbolic links on Windows
39+
*/
3240
private static $symlinkOnWindows = null;
3341

3442
public static function setUpBeforeClass()
3543
{
36-
if ('\\' === DIRECTORY_SEPARATOR && null === self::$symlinkOnWindows) {
37-
$target = tempnam(sys_get_temp_dir(), 'sl');
38-
$link = sys_get_temp_dir().'/sl'.microtime(true).mt_rand();
39-
self::$symlinkOnWindows = @symlink($target, $link) && is_link($link);
40-
@unlink($link);
41-
unlink($target);
44+
if ('\\' === DIRECTORY_SEPARATOR) {
45+
self::$linkOnWindows = true;
46+
$originFile = tempnam(sys_get_temp_dir(), 'li');
47+
$targetFile = tempnam(sys_get_temp_dir(), 'li');
48+
if (true !== @link($originFile, $targetFile)) {
49+
$report = error_get_last();
50+
if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) {
51+
self::$linkOnWindows = false;
52+
}
53+
} else {
54+
@unlink($targetFile);
55+
}
56+
57+
self::$symlinkOnWindows = true;
58+
$originDir = tempnam(sys_get_temp_dir(), 'sl');
59+
$targetDir = tempnam(sys_get_temp_dir(), 'sl');
60+
if (true !== @symlink($originDir, $targetDir)) {
61+
$report = error_get_last();
62+
if (is_array($report) && false !== strpos($report['message'], 'error code(1314)')) {
63+
self::$symlinkOnWindows = false;
64+
}
65+
} else {
66+
@unlink($targetDir);
67+
}
4268
}
4369
}
4470

@@ -100,6 +126,17 @@ protected function getFileGroup($filepath)
100126
$this->markTestSkipped('Unable to retrieve file group name');
101127
}
102128

129+
protected function markAsSkippedIfLinkIsMissing()
130+
{
131+
if (!function_exists('link')) {
132+
$this->markTestSkipped('link is not supported');
133+
}
134+
135+
if ('\\' === DIRECTORY_SEPARATOR && false === self::$linkOnWindows) {
136+
$this->markTestSkipped('link requires "Create hard links" privilege on windows');
137+
}
138+
}
139+
103140
protected function markAsSkippedIfSymlinkIsMissing($relative = false)
104141
{
105142
if ('\\' === DIRECTORY_SEPARATOR && false === self::$symlinkOnWindows) {

0 commit comments

Comments
 (0)
0