From 24eb396f2619fb051e328798b980c904a8d8fc9e Mon Sep 17 00:00:00 2001 From: Romain Neutron Date: Fri, 18 May 2012 16:19:51 +0200 Subject: [PATCH] [Filesystem] Added few new behaviors: - add a IOException and a main filesystem exception interface - whenever an action fails, an IOException is thrown - add access to the second and third arguments of touch() function - add a recursive option for chmod() - add a chown() method - add a chgrp() method - Switch the 'unlink' global function in Filesystem::symlink to Filesystem::remove. BC break: mkdir() function now throws exception in case of failure instead of returning Boolean value. --- .../Exception/ExceptionInterface.php | 24 ++ .../Filesystem/Exception/IOException.php | 24 ++ .../Component/Filesystem/Filesystem.php | 141 ++++++++-- src/Symfony/Component/Filesystem/README.md | 32 ++- .../Filesystem/Tests/FilesystemTest.php | 242 ++++++++++++++++-- 5 files changed, 402 insertions(+), 61 deletions(-) create mode 100644 src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php create mode 100644 src/Symfony/Component/Filesystem/Exception/IOException.php diff --git a/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php b/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php new file mode 100644 index 000000000000..bc9748d752e7 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception interface for all exceptions thrown by the component. + * + * @author Romain Neutron + * + * @api + */ +interface ExceptionInterface +{ + +} diff --git a/src/Symfony/Component/Filesystem/Exception/IOException.php b/src/Symfony/Component/Filesystem/Exception/IOException.php new file mode 100644 index 000000000000..5b27e661eee3 --- /dev/null +++ b/src/Symfony/Component/Filesystem/Exception/IOException.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Filesystem\Exception; + +/** + * Exception class thrown when a filesystem operation failure happens + * + * @author Romain Neutron + * + * @api + */ +class IOException extends \RuntimeException implements ExceptionInterface +{ + +} diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index 04f2558451cb..d57663685ccf 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -28,6 +28,8 @@ class Filesystem * @param string $originFile The original filename * @param string $targetFile The target filename * @param array $override Whether to override an existing file or not + * + * @throws Exception\IOException When copy fails */ public function copy($originFile, $targetFile, $override = false) { @@ -40,7 +42,9 @@ public function copy($originFile, $targetFile, $override = false) } if ($doCopy) { - copy($originFile, $targetFile); + if (true !== @copy($originFile, $targetFile)) { + throw new Exception\IOException(sprintf('Failed to copy %s to %s', $originFile, $targetFile)); + } } } @@ -48,22 +52,21 @@ public function copy($originFile, $targetFile, $override = false) * Creates a directory recursively. * * @param string|array|\Traversable $dirs The directory path - * @param int $mode The directory mode + * @param integer $mode The directory mode * - * @return Boolean true if the directory has been created, false otherwise + * @throws Exception\IOException On any directory creation failure */ public function mkdir($dirs, $mode = 0777) { - $ret = true; foreach ($this->toIterator($dirs) as $dir) { if (is_dir($dir)) { continue; } - $ret = @mkdir($dir, $mode, true) && $ret; + if (true !== @mkdir($dir, $mode, true)) { + throw new Exception\IOException(sprintf('Failed to create %s', $dir)); + } } - - return $ret; } /** @@ -85,14 +88,24 @@ public function exists($files) } /** - * Creates empty files. + * Sets access and modification time of file. * * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create + * @param integer $time The touch time as a unix timestamp + * @param integer $atime The access time as a unix timestamp + * + * @throws Exception\IOException When touch fails */ - public function touch($files) + public function touch($files, $time = null, $atime = null) { + if (null === $time) { + $time = time(); + } + foreach ($this->toIterator($files) as $file) { - touch($file); + if (true !== @touch($file, $time, $atime)) { + throw new Exception\IOException(sprintf('Failed to touch %s', $file)); + } } } @@ -100,6 +113,8 @@ public function touch($files) * Removes files or directories. * * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove + * + * @throws Exception\IOException When removal fails */ public function remove($files) { @@ -113,13 +128,19 @@ public function remove($files) if (is_dir($file) && !is_link($file)) { $this->remove(new \FilesystemIterator($file)); - rmdir($file); + if (true !== @rmdir($file)) { + throw new Exception\IOException(sprintf('Failed to remove directory %s', $file)); + } } else { // https://bugs.php.net/bug.php?id=52176 if (defined('PHP_WINDOWS_VERSION_MAJOR') && is_dir($file)) { - rmdir($file); + if (true !== @rmdir($file)) { + throw new Exception\IOException(sprintf('Failed to remove file %s', $file)); + } } else { - unlink($file); + if (true !== @unlink($file)) { + throw new Exception\IOException(sprintf('Failed to remove file %s', $file)); + } } } } @@ -128,14 +149,76 @@ public function remove($files) /** * Change mode for an array of files or directories. * - * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode - * @param integer $mode The new mode (octal) - * @param integer $umask The mode mask (octal) + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode + * @param integer $mode The new mode (octal) + * @param integer $umask The mode mask (octal) + * @param Boolean $recursive Whether change the mod recursively or not + * + * @throws Exception\IOException When the change fail */ - public function chmod($files, $mode, $umask = 0000) + public function chmod($files, $mode, $umask = 0000, $recursive = false) { foreach ($this->toIterator($files) as $file) { - @chmod($file, $mode & ~$umask); + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); + } + if (true !== @chmod($file, $mode & ~$umask)) { + throw new Exception\IOException(sprintf('Failed to chmod file %s', $file)); + } + } + } + + /** + * Change the owner of an array of files or directories + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner + * @param string $user The new owner user name + * @param Boolean $recursive Whether change the owner recursively or not + * + * @throws Exception\IOException When the change fail + */ + public function chown($files, $user, $recursive = false) + { + foreach ($this->toIterator($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chown(new \FilesystemIterator($file), $user, true); + } + if (is_link($file) && function_exists('lchown')) { + if (true !== @lchown($file, $user)) { + throw new Exception\IOException(sprintf('Failed to chown file %s', $file)); + } + } else { + if (true !== @chown($file, $user)) { + throw new Exception\IOException(sprintf('Failed to chown file %s', $file)); + } + } + } + } + + /** + * Change the group of an array of files or directories + * + * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group + * @param string $group The group name + * @param Boolean $recursive Whether change the group recursively or not + * + * @throws Exception\IOException When the change fail + */ + public function chgrp($files, $group, $recursive = false) + { + foreach ($this->toIterator($files) as $file) { + if ($recursive && is_dir($file) && !is_link($file)) { + $this->chgrp(new \FilesystemIterator($file), $group, true); + } + if (is_link($file) && function_exists('lchgrp')) { + if (true !== @lchgrp($file, $group)) { + throw new Exception\IOException(sprintf('Failed to chgrp file %s', $file)); + } + } else { + if (true !== @chgrp($file, $group)) { + throw new Exception\IOException(sprintf('Failed to chgrp file %s', $file)); + } + } } } @@ -145,18 +228,18 @@ public function chmod($files, $mode, $umask = 0000) * @param string $origin The origin filename * @param string $target The new filename * - * @throws \RuntimeException When target file already exists - * @throws \RuntimeException When origin cannot be renamed + * @throws Exception\IOException When target file already exists + * @throws Exception\IOException When origin cannot be renamed */ public function rename($origin, $target) { // we check that target does not exist if (is_readable($target)) { - throw new \RuntimeException(sprintf('Cannot rename because the target "%s" already exist.', $target)); + throw new Exception\IOException(sprintf('Cannot rename because the target "%s" already exist.', $target)); } - if (false === @rename($origin, $target)) { - throw new \RuntimeException(sprintf('Cannot rename "%s" to "%s".', $origin, $target)); + if (true !== @rename($origin, $target)) { + throw new Exception\IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target)); } } @@ -166,6 +249,8 @@ public function rename($origin, $target) * @param string $originDir The origin directory path * @param string $targetDir The symbolic link name * @param Boolean $copyOnWindows Whether to copy files if on Windows + * + * @throws Exception\IOException When symlink fails */ public function symlink($originDir, $targetDir, $copyOnWindows = false) { @@ -180,14 +265,16 @@ public function symlink($originDir, $targetDir, $copyOnWindows = false) $ok = false; if (is_link($targetDir)) { if (readlink($targetDir) != $originDir) { - unlink($targetDir); + $this->remove($targetDir); } else { $ok = true; } } if (!$ok) { - symlink($originDir, $targetDir); + if (true !== @symlink($originDir, $targetDir)) { + throw new Exception\IOException(sprintf('Failed to create symbolic link from %s to %s', $originDir, $targetDir)); + } } } @@ -235,7 +322,7 @@ public function makePathRelative($endPath, $startPath) * - $options['override'] Whether to override an existing file on copy or not (see copy()) * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink()) * - * @throws \RuntimeException When file type is unknown + * @throws Exception\IOException When file type is unknown */ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()) { @@ -262,7 +349,7 @@ public function mirror($originDir, $targetDir, \Traversable $iterator = null, $o } elseif (is_file($file) || ($copyOnWindows && is_link($file))) { $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); } else { - throw new \RuntimeException(sprintf('Unable to guess "%s" file type.', $file)); + throw new Exception\IOException(sprintf('Unable to guess "%s" file type.', $file)); } } } diff --git a/src/Symfony/Component/Filesystem/README.md b/src/Symfony/Component/Filesystem/README.md index e34bb8f05f9c..5c654aad35be 100644 --- a/src/Symfony/Component/Filesystem/README.md +++ b/src/Symfony/Component/Filesystem/README.md @@ -3,29 +3,37 @@ Filesystem Component Filesystem provides basic utility to manipulate the file system: - use Symfony\Component\Filesystem\Filesystem; +```php +copy($originFile, $targetFile, $override = false); +$filesystem = new Filesystem(); - $filesystem->mkdir($dirs, $mode = 0777); +$filesystem->copy($originFile, $targetFile, $override = false); - $filesystem->touch($files); +$filesystem->mkdir($dirs, $mode = 0777); - $filesystem->remove($files); +$filesystem->touch($files, $time = null, $atime = null); - $filesystem->chmod($files, $mode, $umask = 0000); +$filesystem->remove($files); - $filesystem->rename($origin, $target); +$filesystem->chmod($files, $mode, $umask = 0000, $recursive = false); - $filesystem->symlink($originDir, $targetDir, $copyOnWindows = false); +$filesystem->chown($files, $user, $recursive = false); - $filesystem->makePathRelative($endPath, $startPath); +$filesystem->chgrp($files, $group, $recursive = false); - $filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()); +$filesystem->rename($origin, $target); - $filesystem->isAbsolutePath($file); +$filesystem->symlink($originDir, $targetDir, $copyOnWindows = false); + +$filesystem->makePathRelative($endPath, $startPath); + +$filesystem->mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()); + +$filesystem->isAbsolutePath($file); +``` Resources --------- diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index e3d5408d1c91..a29155c4bd7e 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -70,6 +70,17 @@ public function testCopyCreatesNewFile() $this->assertEquals('SOURCE FILE', file_get_contents($targetFilePath)); } + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testCopyFails() + { + $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file'; + $targetFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_target_file'; + + $this->filesystem->copy($sourceFilePath, $targetFilePath); + } + public function testCopyOverridesExistingFileIfModified() { $sourceFilePath = $this->workspace.DIRECTORY_SEPARATOR.'copy_source_file'; @@ -144,9 +155,8 @@ public function testMkdirCreatesDirectoriesRecursively() .DIRECTORY_SEPARATOR.'directory' .DIRECTORY_SEPARATOR.'sub_directory'; - $result = $this->filesystem->mkdir($directory); + $this->filesystem->mkdir($directory); - $this->assertTrue($result); $this->assertTrue(is_dir($directory)); } @@ -157,9 +167,8 @@ public function testMkdirCreatesDirectoriesFromArray() $basePath.'1', $basePath.'2', $basePath.'3' ); - $result = $this->filesystem->mkdir($directories); + $this->filesystem->mkdir($directories); - $this->assertTrue($result); $this->assertTrue(is_dir($basePath.'1')); $this->assertTrue(is_dir($basePath.'2')); $this->assertTrue(is_dir($basePath.'3')); @@ -172,30 +181,24 @@ public function testMkdirCreatesDirectoriesFromTraversableObject() $basePath.'1', $basePath.'2', $basePath.'3' )); - $result = $this->filesystem->mkdir($directories); + $this->filesystem->mkdir($directories); - $this->assertTrue($result); $this->assertTrue(is_dir($basePath.'1')); $this->assertTrue(is_dir($basePath.'2')); $this->assertTrue(is_dir($basePath.'3')); } - public function testMkdirCreatesDirectoriesEvenIfItFailsToCreateOneOfThem() + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testMkdirCreatesDirectoriesFails() { $basePath = $this->workspace.DIRECTORY_SEPARATOR; - $directories = array( - $basePath.'1', $basePath.'2', $basePath.'3' - ); + $dir = $basePath.'2'; - // create a file to make that directory cannot be created - file_put_contents($basePath.'2', ''); + file_put_contents($dir, ''); - $result = $this->filesystem->mkdir($directories); - - $this->assertFalse($result); - $this->assertTrue(is_dir($basePath.'1')); - $this->assertFalse(is_dir($basePath.'2')); - $this->assertTrue(is_dir($basePath.'3')); + $this->filesystem->mkdir($dir); } public function testTouchCreatesEmptyFile() @@ -207,6 +210,16 @@ public function testTouchCreatesEmptyFile() $this->assertFileExists($file); } + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testTouchFails() + { + $file = $this->workspace.DIRECTORY_SEPARATOR.'1'.DIRECTORY_SEPARATOR.'2'; + + $this->filesystem->touch($file); + } + public function testTouchCreatesEmptyFilesFromArray() { $basePath = $this->workspace.DIRECTORY_SEPARATOR; @@ -367,11 +380,41 @@ public function testChmodChangesFileMode() { $this->markAsSkippedIfChmodIsMissing(); - $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + $file = $dir.DIRECTORY_SEPARATOR.'file'; + touch($file); + + $this->filesystem->chmod($file, 0400); + $this->filesystem->chmod($dir, 0753); + + $this->assertEquals(753, $this->getFilePermisions($dir)); + $this->assertEquals(400, $this->getFilePermisions($file)); + } + + public function testChmodWrongMod() + { + $this->markAsSkippedIfChmodIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'file'; + touch($dir); + + $this->filesystem->chmod($dir, 'Wrongmode'); + } + + public function testChmodRecursive() + { + $this->markAsSkippedIfChmodIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + $file = $dir.DIRECTORY_SEPARATOR.'file'; touch($file); - $this->filesystem->chmod($file, 0753); + $this->filesystem->chmod($file, 0400, 0000, true); + $this->filesystem->chmod($dir, 0753, 0000, true); + $this->assertEquals(753, $this->getFilePermisions($dir)); $this->assertEquals(753, $this->getFilePermisions($file)); } @@ -420,6 +463,138 @@ public function testChmodChangesModeOfTraversableFileObject() $this->assertEquals(753, $this->getFilePermisions($directory)); } + public function testChown() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + + $this->filesystem->chown($dir, $this->getFileOwner($dir)); + } + + public function testChownRecursive() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + $file = $dir.DIRECTORY_SEPARATOR.'file'; + touch($file); + + $this->filesystem->chown($dir, $this->getFileOwner($dir), true); + } + + public function testChownSymlink() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->symlink($file, $link); + + $this->filesystem->chown($link, $this->getFileOwner($link)); + } + + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testChownSymlinkFails() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->symlink($file, $link); + + $this->filesystem->chown($link, 'user' . time() . mt_rand(1000, 9999)); + } + + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testChownFail() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + + $this->filesystem->chown($dir, 'user' . time() . mt_rand(1000, 9999)); + } + + public function testChgrp() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + + $this->filesystem->chgrp($dir, $this->getFileGroup($dir)); + } + + public function testChgrpRecursive() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + $file = $dir.DIRECTORY_SEPARATOR.'file'; + touch($file); + + $this->filesystem->chgrp($dir, $this->getFileGroup($dir), true); + } + + public function testChgrpSymlink() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->symlink($file, $link); + + $this->filesystem->chgrp($link, $this->getFileGroup($link)); + } + + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testChgrpSymlinkFails() + { + $this->markAsSkippedIfSymlinkIsMissing(); + + $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; + $link = $this->workspace.DIRECTORY_SEPARATOR.'link'; + + touch($file); + + $this->filesystem->symlink($file, $link); + + $this->filesystem->chgrp($link, 'user' . time() . mt_rand(1000, 9999)); + } + + /** + * @expectedException Symfony\Component\Filesystem\Exception\IOException + */ + public function testChgrpFail() + { + $this->markAsSkippedIfPosixIsMissing(); + + $dir = $this->workspace.DIRECTORY_SEPARATOR.'dir'; + mkdir($dir); + + $this->filesystem->chgrp($dir, 'user' . time() . mt_rand(1000, 9999)); + } + public function testRename() { $file = $this->workspace.DIRECTORY_SEPARATOR.'file'; @@ -433,7 +608,7 @@ public function testRename() } /** - * @expectedException \RuntimeException + * @expectedException Symfony\Component\Filesystem\Exception\IOException */ public function testRenameThrowsExceptionIfTargetAlreadyExists() { @@ -447,7 +622,7 @@ public function testRenameThrowsExceptionIfTargetAlreadyExists() } /** - * @expectedException \RuntimeException + * @expectedException Symfony\Component\Filesystem\Exception\IOException */ public function testRenameThrowsExceptionOnError() { @@ -644,6 +819,22 @@ private function getFilePermisions($filePath) return (int) substr(sprintf('%o', fileperms($filePath)), -3); } + private function getFileOwner($filepath) + { + $infos = stat($filepath); + if ($datas = posix_getpwuid($infos['uid'])) { + return $datas['name']; + } + } + + private function getFileGroup($filepath) + { + $infos = stat($filepath); + if ($datas = posix_getgrgid($infos['gid'])) { + return $datas['name']; + } + } + private function markAsSkippedIfSymlinkIsMissing() { if (!function_exists('symlink')) { @@ -657,4 +848,11 @@ private function markAsSkippedIfChmodIsMissing() $this->markTestSkipped('chmod is not supported on windows'); } } + + private function markAsSkippedIfPosixIsMissing() + { + if (defined('PHP_WINDOWS_VERSION_MAJOR')) { + $this->markTestSkipped('Posix uids are not available on windows'); + } + } }