Filesystem.php (19879B)
1 <?php 2 3 /* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12 namespace Symfony\Component\Filesystem; 13 14 use Symfony\Component\Filesystem\Exception\IOException; 15 use Symfony\Component\Filesystem\Exception\FileNotFoundException; 16 17 /** 18 * Provides basic utility to manipulate the file system. 19 * 20 * @author Fabien Potencier <fabien@symfony.com> 21 */ 22 class Filesystem 23 { 24 /** 25 * Copies a file. 26 * 27 * This method only copies the file if the origin file is newer than the target file. 28 * 29 * By default, if the target already exists, it is not overridden. 30 * 31 * @param string $originFile The original filename 32 * @param string $targetFile The target filename 33 * @param bool $override Whether to override an existing file or not 34 * 35 * @throws FileNotFoundException When originFile doesn't exist 36 * @throws IOException When copy fails 37 */ 38 public function copy($originFile, $targetFile, $override = false) 39 { 40 if (stream_is_local($originFile) && !is_file($originFile)) { 41 throw new FileNotFoundException(sprintf('Failed to copy "%s" because file does not exist.', $originFile), 0, null, $originFile); 42 } 43 44 $this->mkdir(dirname($targetFile)); 45 46 $doCopy = true; 47 if (!$override && null === parse_url($originFile, PHP_URL_HOST) && is_file($targetFile)) { 48 $doCopy = filemtime($originFile) > filemtime($targetFile); 49 } 50 51 if ($doCopy) { 52 // https://bugs.php.net/bug.php?id=64634 53 if (false === $source = @fopen($originFile, 'r')) { 54 throw new IOException(sprintf('Failed to copy "%s" to "%s" because source file could not be opened for reading.', $originFile, $targetFile), 0, null, $originFile); 55 } 56 57 // Stream context created to allow files overwrite when using FTP stream wrapper - disabled by default 58 if (false === $target = @fopen($targetFile, 'w', null, stream_context_create(array('ftp' => array('overwrite' => true))))) { 59 throw new IOException(sprintf('Failed to copy "%s" to "%s" because target file could not be opened for writing.', $originFile, $targetFile), 0, null, $originFile); 60 } 61 62 $bytesCopied = stream_copy_to_stream($source, $target); 63 fclose($source); 64 fclose($target); 65 unset($source, $target); 66 67 if (!is_file($targetFile)) { 68 throw new IOException(sprintf('Failed to copy "%s" to "%s".', $originFile, $targetFile), 0, null, $originFile); 69 } 70 71 // Like `cp`, preserve executable permission bits 72 @chmod($targetFile, fileperms($targetFile) | (fileperms($originFile) & 0111)); 73 74 if (stream_is_local($originFile) && $bytesCopied !== ($bytesOrigin = filesize($originFile))) { 75 throw new IOException(sprintf('Failed to copy the whole content of "%s" to "%s" (%g of %g bytes copied).', $originFile, $targetFile, $bytesCopied, $bytesOrigin), 0, null, $originFile); 76 } 77 } 78 } 79 80 /** 81 * Creates a directory recursively. 82 * 83 * @param string|array|\Traversable $dirs The directory path 84 * @param int $mode The directory mode 85 * 86 * @throws IOException On any directory creation failure 87 */ 88 public function mkdir($dirs, $mode = 0777) 89 { 90 foreach ($this->toIterator($dirs) as $dir) { 91 if (is_dir($dir)) { 92 continue; 93 } 94 95 if (true !== @mkdir($dir, $mode, true)) { 96 $error = error_get_last(); 97 if (!is_dir($dir)) { 98 // The directory was not created by a concurrent process. Let's throw an exception with a developer friendly error message if we have one 99 if ($error) { 100 throw new IOException(sprintf('Failed to create "%s": %s.', $dir, $error['message']), 0, null, $dir); 101 } 102 throw new IOException(sprintf('Failed to create "%s"', $dir), 0, null, $dir); 103 } 104 } 105 } 106 } 107 108 /** 109 * Checks the existence of files or directories. 110 * 111 * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to check 112 * 113 * @return bool true if the file exists, false otherwise 114 */ 115 public function exists($files) 116 { 117 foreach ($this->toIterator($files) as $file) { 118 if (!file_exists($file)) { 119 return false; 120 } 121 } 122 123 return true; 124 } 125 126 /** 127 * Sets access and modification time of file. 128 * 129 * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to create 130 * @param int $time The touch time as a Unix timestamp 131 * @param int $atime The access time as a Unix timestamp 132 * 133 * @throws IOException When touch fails 134 */ 135 public function touch($files, $time = null, $atime = null) 136 { 137 foreach ($this->toIterator($files) as $file) { 138 $touch = $time ? @touch($file, $time, $atime) : @touch($file); 139 if (true !== $touch) { 140 throw new IOException(sprintf('Failed to touch "%s".', $file), 0, null, $file); 141 } 142 } 143 } 144 145 /** 146 * Removes files or directories. 147 * 148 * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to remove 149 * 150 * @throws IOException When removal fails 151 */ 152 public function remove($files) 153 { 154 $files = iterator_to_array($this->toIterator($files)); 155 $files = array_reverse($files); 156 foreach ($files as $file) { 157 if (!file_exists($file) && !is_link($file)) { 158 continue; 159 } 160 161 if (is_dir($file) && !is_link($file)) { 162 $this->remove(new \FilesystemIterator($file)); 163 164 if (true !== @rmdir($file)) { 165 throw new IOException(sprintf('Failed to remove directory "%s".', $file), 0, null, $file); 166 } 167 } else { 168 // https://bugs.php.net/bug.php?id=52176 169 if ('\\' === DIRECTORY_SEPARATOR && is_dir($file)) { 170 if (true !== @rmdir($file)) { 171 throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file); 172 } 173 } else { 174 if (true !== @unlink($file)) { 175 throw new IOException(sprintf('Failed to remove file "%s".', $file), 0, null, $file); 176 } 177 } 178 } 179 } 180 } 181 182 /** 183 * Change mode for an array of files or directories. 184 * 185 * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change mode 186 * @param int $mode The new mode (octal) 187 * @param int $umask The mode mask (octal) 188 * @param bool $recursive Whether change the mod recursively or not 189 * 190 * @throws IOException When the change fail 191 */ 192 public function chmod($files, $mode, $umask = 0000, $recursive = false) 193 { 194 foreach ($this->toIterator($files) as $file) { 195 if ($recursive && is_dir($file) && !is_link($file)) { 196 $this->chmod(new \FilesystemIterator($file), $mode, $umask, true); 197 } 198 if (true !== @chmod($file, $mode & ~$umask)) { 199 throw new IOException(sprintf('Failed to chmod file "%s".', $file), 0, null, $file); 200 } 201 } 202 } 203 204 /** 205 * Change the owner of an array of files or directories. 206 * 207 * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change owner 208 * @param string $user The new owner user name 209 * @param bool $recursive Whether change the owner recursively or not 210 * 211 * @throws IOException When the change fail 212 */ 213 public function chown($files, $user, $recursive = false) 214 { 215 foreach ($this->toIterator($files) as $file) { 216 if ($recursive && is_dir($file) && !is_link($file)) { 217 $this->chown(new \FilesystemIterator($file), $user, true); 218 } 219 if (is_link($file) && function_exists('lchown')) { 220 if (true !== @lchown($file, $user)) { 221 throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); 222 } 223 } else { 224 if (true !== @chown($file, $user)) { 225 throw new IOException(sprintf('Failed to chown file "%s".', $file), 0, null, $file); 226 } 227 } 228 } 229 } 230 231 /** 232 * Change the group of an array of files or directories. 233 * 234 * @param string|array|\Traversable $files A filename, an array of files, or a \Traversable instance to change group 235 * @param string $group The group name 236 * @param bool $recursive Whether change the group recursively or not 237 * 238 * @throws IOException When the change fail 239 */ 240 public function chgrp($files, $group, $recursive = false) 241 { 242 foreach ($this->toIterator($files) as $file) { 243 if ($recursive && is_dir($file) && !is_link($file)) { 244 $this->chgrp(new \FilesystemIterator($file), $group, true); 245 } 246 if (is_link($file) && function_exists('lchgrp')) { 247 if (true !== @lchgrp($file, $group) || (defined('HHVM_VERSION') && !posix_getgrnam($group))) { 248 throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); 249 } 250 } else { 251 if (true !== @chgrp($file, $group)) { 252 throw new IOException(sprintf('Failed to chgrp file "%s".', $file), 0, null, $file); 253 } 254 } 255 } 256 } 257 258 /** 259 * Renames a file or a directory. 260 * 261 * @param string $origin The origin filename or directory 262 * @param string $target The new filename or directory 263 * @param bool $overwrite Whether to overwrite the target if it already exists 264 * 265 * @throws IOException When target file or directory already exists 266 * @throws IOException When origin cannot be renamed 267 */ 268 public function rename($origin, $target, $overwrite = false) 269 { 270 // we check that target does not exist 271 if (!$overwrite && is_readable($target)) { 272 throw new IOException(sprintf('Cannot rename because the target "%s" already exists.', $target), 0, null, $target); 273 } 274 275 if (true !== @rename($origin, $target)) { 276 throw new IOException(sprintf('Cannot rename "%s" to "%s".', $origin, $target), 0, null, $target); 277 } 278 } 279 280 /** 281 * Creates a symbolic link or copy a directory. 282 * 283 * @param string $originDir The origin directory path 284 * @param string $targetDir The symbolic link name 285 * @param bool $copyOnWindows Whether to copy files if on Windows 286 * 287 * @throws IOException When symlink fails 288 */ 289 public function symlink($originDir, $targetDir, $copyOnWindows = false) 290 { 291 if ('\\' === DIRECTORY_SEPARATOR && $copyOnWindows) { 292 $this->mirror($originDir, $targetDir); 293 294 return; 295 } 296 297 $this->mkdir(dirname($targetDir)); 298 299 $ok = false; 300 if (is_link($targetDir)) { 301 if (readlink($targetDir) != $originDir) { 302 $this->remove($targetDir); 303 } else { 304 $ok = true; 305 } 306 } 307 308 if (!$ok && true !== @symlink($originDir, $targetDir)) { 309 $report = error_get_last(); 310 if (is_array($report)) { 311 if ('\\' === DIRECTORY_SEPARATOR && false !== strpos($report['message'], 'error code(1314)')) { 312 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); 313 } 314 } 315 throw new IOException(sprintf('Failed to create symbolic link from "%s" to "%s".', $originDir, $targetDir), 0, null, $targetDir); 316 } 317 } 318 319 /** 320 * Given an existing path, convert it to a path relative to a given starting path. 321 * 322 * @param string $endPath Absolute path of target 323 * @param string $startPath Absolute path where traversal begins 324 * 325 * @return string Path of target relative to starting path 326 */ 327 public function makePathRelative($endPath, $startPath) 328 { 329 // Normalize separators on Windows 330 if ('\\' === DIRECTORY_SEPARATOR) { 331 $endPath = str_replace('\\', '/', $endPath); 332 $startPath = str_replace('\\', '/', $startPath); 333 } 334 335 // Split the paths into arrays 336 $startPathArr = explode('/', trim($startPath, '/')); 337 $endPathArr = explode('/', trim($endPath, '/')); 338 339 // Find for which directory the common path stops 340 $index = 0; 341 while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) { 342 ++$index; 343 } 344 345 // Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels) 346 $depth = count($startPathArr) - $index; 347 348 // Repeated "../" for each level need to reach the common path 349 $traverser = str_repeat('../', $depth); 350 351 $endPathRemainder = implode('/', array_slice($endPathArr, $index)); 352 353 // Construct $endPath from traversing to the common path, then to the remaining $endPath 354 $relativePath = $traverser.('' !== $endPathRemainder ? $endPathRemainder.'/' : ''); 355 356 return '' === $relativePath ? './' : $relativePath; 357 } 358 359 /** 360 * Mirrors a directory to another. 361 * 362 * @param string $originDir The origin directory 363 * @param string $targetDir The target directory 364 * @param \Traversable $iterator A Traversable instance 365 * @param array $options An array of boolean options 366 * Valid options are: 367 * - $options['override'] Whether to override an existing file on copy or not (see copy()) 368 * - $options['copy_on_windows'] Whether to copy files instead of links on Windows (see symlink()) 369 * - $options['delete'] Whether to delete files that are not in the source directory (defaults to false) 370 * 371 * @throws IOException When file type is unknown 372 */ 373 public function mirror($originDir, $targetDir, \Traversable $iterator = null, $options = array()) 374 { 375 $targetDir = rtrim($targetDir, '/\\'); 376 $originDir = rtrim($originDir, '/\\'); 377 378 // Iterate in destination folder to remove obsolete entries 379 if ($this->exists($targetDir) && isset($options['delete']) && $options['delete']) { 380 $deleteIterator = $iterator; 381 if (null === $deleteIterator) { 382 $flags = \FilesystemIterator::SKIP_DOTS; 383 $deleteIterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($targetDir, $flags), \RecursiveIteratorIterator::CHILD_FIRST); 384 } 385 foreach ($deleteIterator as $file) { 386 $origin = str_replace($targetDir, $originDir, $file->getPathname()); 387 if (!$this->exists($origin)) { 388 $this->remove($file); 389 } 390 } 391 } 392 393 $copyOnWindows = false; 394 if (isset($options['copy_on_windows'])) { 395 $copyOnWindows = $options['copy_on_windows']; 396 } 397 398 if (null === $iterator) { 399 $flags = $copyOnWindows ? \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::FOLLOW_SYMLINKS : \FilesystemIterator::SKIP_DOTS; 400 $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($originDir, $flags), \RecursiveIteratorIterator::SELF_FIRST); 401 } 402 403 if ($this->exists($originDir)) { 404 $this->mkdir($targetDir); 405 } 406 407 foreach ($iterator as $file) { 408 $target = str_replace($originDir, $targetDir, $file->getPathname()); 409 410 if ($copyOnWindows) { 411 if (is_link($file) || is_file($file)) { 412 $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); 413 } elseif (is_dir($file)) { 414 $this->mkdir($target); 415 } else { 416 throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); 417 } 418 } else { 419 if (is_link($file)) { 420 $this->symlink($file->getRealPath(), $target); 421 } elseif (is_dir($file)) { 422 $this->mkdir($target); 423 } elseif (is_file($file)) { 424 $this->copy($file, $target, isset($options['override']) ? $options['override'] : false); 425 } else { 426 throw new IOException(sprintf('Unable to guess "%s" file type.', $file), 0, null, $file); 427 } 428 } 429 } 430 } 431 432 /** 433 * Returns whether the file path is an absolute path. 434 * 435 * @param string $file A file path 436 * 437 * @return bool 438 */ 439 public function isAbsolutePath($file) 440 { 441 return (strspn($file, '/\\', 0, 1) 442 || (strlen($file) > 3 && ctype_alpha($file[0]) 443 && substr($file, 1, 1) === ':' 444 && (strspn($file, '/\\', 2, 1)) 445 ) 446 || null !== parse_url($file, PHP_URL_SCHEME) 447 ); 448 } 449 450 /** 451 * Atomically dumps content into a file. 452 * 453 * @param string $filename The file to be written to. 454 * @param string $content The data to write into the file. 455 * @param null|int $mode The file mode (octal). If null, file permissions are not modified 456 * Deprecated since version 2.3.12, to be removed in 3.0. 457 * 458 * @throws IOException If the file cannot be written to. 459 */ 460 public function dumpFile($filename, $content, $mode = 0666) 461 { 462 $dir = dirname($filename); 463 464 if (!is_dir($dir)) { 465 $this->mkdir($dir); 466 } elseif (!is_writable($dir)) { 467 throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); 468 } 469 470 $tmpFile = tempnam($dir, basename($filename)); 471 472 if (false === @file_put_contents($tmpFile, $content)) { 473 throw new IOException(sprintf('Failed to write file "%s".', $filename), 0, null, $filename); 474 } 475 476 $this->rename($tmpFile, $filename, true); 477 if (null !== $mode) { 478 if (func_num_args() > 2) { 479 @trigger_error('Support for modifying file permissions is deprecated since version 2.3.12 and will be removed in 3.0.', E_USER_DEPRECATED); 480 } 481 482 $this->chmod($filename, $mode); 483 } 484 } 485 486 /** 487 * @param mixed $files 488 * 489 * @return \Traversable 490 */ 491 private function toIterator($files) 492 { 493 if (!$files instanceof \Traversable) { 494 $files = new \ArrayObject(is_array($files) ? $files : array($files)); 495 } 496 497 return $files; 498 } 499 }