FileSystem.php 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (https://nette.org)
  4. * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  5. */
  6. declare(strict_types=1);
  7. namespace Nette\Utils;
  8. use Nette;
  9. /**
  10. * File system tool.
  11. */
  12. final class FileSystem
  13. {
  14. use Nette\StaticClass;
  15. /**
  16. * Creates a directory if it doesn't exist.
  17. * @throws Nette\IOException on error occurred
  18. */
  19. public static function createDir(string $dir, int $mode = 0777): void
  20. {
  21. if (!is_dir($dir) && !@mkdir($dir, $mode, true) && !is_dir($dir)) { // @ - dir may already exist
  22. throw new Nette\IOException("Unable to create directory '$dir' with mode " . decoct($mode) . '. ' . Helpers::getLastError());
  23. }
  24. }
  25. /**
  26. * Copies a file or a directory. Overwrites existing files and directories by default.
  27. * @throws Nette\IOException on error occurred
  28. * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists
  29. */
  30. public static function copy(string $origin, string $target, bool $overwrite = true): void
  31. {
  32. if (stream_is_local($origin) && !file_exists($origin)) {
  33. throw new Nette\IOException("File or directory '$origin' not found.");
  34. } elseif (!$overwrite && file_exists($target)) {
  35. throw new Nette\InvalidStateException("File or directory '$target' already exists.");
  36. } elseif (is_dir($origin)) {
  37. static::createDir($target);
  38. foreach (new \FilesystemIterator($target) as $item) {
  39. static::delete($item->getPathname());
  40. }
  41. foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
  42. if ($item->isDir()) {
  43. static::createDir($target . '/' . $iterator->getSubPathName());
  44. } else {
  45. static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName());
  46. }
  47. }
  48. } else {
  49. static::createDir(dirname($target));
  50. if (
  51. ($s = @fopen($origin, 'rb'))
  52. && ($d = @fopen($target, 'wb'))
  53. && @stream_copy_to_stream($s, $d) === false
  54. ) { // @ is escalated to exception
  55. throw new Nette\IOException("Unable to copy file '$origin' to '$target'. " . Helpers::getLastError());
  56. }
  57. }
  58. }
  59. /**
  60. * Deletes a file or directory if exists.
  61. * @throws Nette\IOException on error occurred
  62. */
  63. public static function delete(string $path): void
  64. {
  65. if (is_file($path) || is_link($path)) {
  66. $func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink';
  67. if (!@$func($path)) { // @ is escalated to exception
  68. throw new Nette\IOException("Unable to delete '$path'. " . Helpers::getLastError());
  69. }
  70. } elseif (is_dir($path)) {
  71. foreach (new \FilesystemIterator($path) as $item) {
  72. static::delete($item->getPathname());
  73. }
  74. if (!@rmdir($path)) { // @ is escalated to exception
  75. throw new Nette\IOException("Unable to delete directory '$path'. " . Helpers::getLastError());
  76. }
  77. }
  78. }
  79. /**
  80. * Renames or moves a file or a directory. Overwrites existing files and directories by default.
  81. * @throws Nette\IOException on error occurred
  82. * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists
  83. */
  84. public static function rename(string $origin, string $target, bool $overwrite = true): void
  85. {
  86. if (!$overwrite && file_exists($target)) {
  87. throw new Nette\InvalidStateException("File or directory '$target' already exists.");
  88. } elseif (!file_exists($origin)) {
  89. throw new Nette\IOException("File or directory '$origin' not found.");
  90. } else {
  91. static::createDir(dirname($target));
  92. if (realpath($origin) !== realpath($target)) {
  93. static::delete($target);
  94. }
  95. if (!@rename($origin, $target)) { // @ is escalated to exception
  96. throw new Nette\IOException("Unable to rename file or directory '$origin' to '$target'. " . Helpers::getLastError());
  97. }
  98. }
  99. }
  100. /**
  101. * Reads the content of a file.
  102. * @throws Nette\IOException on error occurred
  103. */
  104. public static function read(string $file): string
  105. {
  106. $content = @file_get_contents($file); // @ is escalated to exception
  107. if ($content === false) {
  108. throw new Nette\IOException("Unable to read file '$file'. " . Helpers::getLastError());
  109. }
  110. return $content;
  111. }
  112. /**
  113. * Writes the string to a file.
  114. * @throws Nette\IOException on error occurred
  115. */
  116. public static function write(string $file, string $content, ?int $mode = 0666): void
  117. {
  118. static::createDir(dirname($file));
  119. if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
  120. throw new Nette\IOException("Unable to write file '$file'. " . Helpers::getLastError());
  121. }
  122. if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception
  123. throw new Nette\IOException("Unable to chmod file '$file' to mode " . decoct($mode) . '. ' . Helpers::getLastError());
  124. }
  125. }
  126. /**
  127. * Fixes permissions to a specific file or directory. Directories can be fixed recursively.
  128. * @throws Nette\IOException on error occurred
  129. */
  130. public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void
  131. {
  132. if (is_file($path)) {
  133. if (!@chmod($path, $fileMode)) { // @ is escalated to exception
  134. throw new Nette\IOException("Unable to chmod file '$path' to mode " . decoct($fileMode) . '. ' . Helpers::getLastError());
  135. }
  136. } elseif (is_dir($path)) {
  137. foreach (new \FilesystemIterator($path) as $item) {
  138. static::makeWritable($item->getPathname(), $dirMode, $fileMode);
  139. }
  140. if (!@chmod($path, $dirMode)) { // @ is escalated to exception
  141. throw new Nette\IOException("Unable to chmod directory '$path' to mode " . decoct($dirMode) . '. ' . Helpers::getLastError());
  142. }
  143. } else {
  144. throw new Nette\IOException("File or directory '$path' not found.");
  145. }
  146. }
  147. /**
  148. * Determines if the path is absolute.
  149. */
  150. public static function isAbsolute(string $path): bool
  151. {
  152. return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
  153. }
  154. /**
  155. * Normalizes `..` and `.` and directory separators in path.
  156. */
  157. public static function normalizePath(string $path): string
  158. {
  159. $parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path);
  160. $res = [];
  161. foreach ($parts as $part) {
  162. if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
  163. array_pop($res);
  164. } elseif ($part !== '.') {
  165. $res[] = $part;
  166. }
  167. }
  168. return $res === ['']
  169. ? DIRECTORY_SEPARATOR
  170. : implode(DIRECTORY_SEPARATOR, $res);
  171. }
  172. /**
  173. * Joins all segments of the path and normalizes the result.
  174. */
  175. public static function joinPaths(string ...$paths): string
  176. {
  177. return self::normalizePath(implode('/', $paths));
  178. }
  179. }