1<?php 2 3/** 4 * This file is part of the Nette Framework (https://nette.org) 5 * Copyright (c) 2004 David Grudl (https://davidgrudl.com) 6 */ 7 8declare(strict_types=1); 9 10namespace Nette\Utils; 11 12use Nette; 13 14 15/** 16 * File system tool. 17 */ 18final class FileSystem 19{ 20 use Nette\StaticClass; 21 22 /** 23 * Creates a directory if it does not exist, including parent directories. 24 * @throws Nette\IOException on error occurred 25 */ 26 public static function createDir(string $dir, int $mode = 0777): void 27 { 28 if (!is_dir($dir) && !@mkdir($dir, $mode, true) && !is_dir($dir)) { // @ - dir may already exist 29 throw new Nette\IOException(sprintf( 30 "Unable to create directory '%s' with mode %s. %s", 31 self::normalizePath($dir), 32 decoct($mode), 33 Helpers::getLastError(), 34 )); 35 } 36 } 37 38 39 /** 40 * Copies a file or an entire directory. Overwrites existing files and directories by default. 41 * @throws Nette\IOException on error occurred 42 * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists 43 */ 44 public static function copy(string $origin, string $target, bool $overwrite = true): void 45 { 46 if (stream_is_local($origin) && !file_exists($origin)) { 47 throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin))); 48 49 } elseif (!$overwrite && file_exists($target)) { 50 throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target))); 51 52 } elseif (is_dir($origin)) { 53 static::createDir($target); 54 foreach (new \FilesystemIterator($target) as $item) { 55 static::delete($item->getPathname()); 56 } 57 58 foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) { 59 if ($item->isDir()) { 60 static::createDir($target . '/' . $iterator->getSubPathName()); 61 } else { 62 static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathName()); 63 } 64 } 65 } else { 66 static::createDir(dirname($target)); 67 if (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === false) { // @ is escalated to exception 68 throw new Nette\IOException(sprintf( 69 "Unable to copy file '%s' to '%s'. %s", 70 self::normalizePath($origin), 71 self::normalizePath($target), 72 Helpers::getLastError(), 73 )); 74 } 75 } 76 } 77 78 79 /** 80 * Opens file and returns resource. 81 * @return resource 82 * @throws Nette\IOException on error occurred 83 */ 84 public static function open(string $path, string $mode) 85 { 86 $f = @fopen($path, $mode); // @ is escalated to exception 87 if (!$f) { 88 throw new Nette\IOException(sprintf( 89 "Unable to open file '%s'. %s", 90 self::normalizePath($path), 91 Helpers::getLastError(), 92 )); 93 } 94 return $f; 95 } 96 97 98 /** 99 * Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first. 100 * @throws Nette\IOException on error occurred 101 */ 102 public static function delete(string $path): void 103 { 104 if (is_file($path) || is_link($path)) { 105 $func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink'; 106 if (!@$func($path)) { // @ is escalated to exception 107 throw new Nette\IOException(sprintf( 108 "Unable to delete '%s'. %s", 109 self::normalizePath($path), 110 Helpers::getLastError(), 111 )); 112 } 113 } elseif (is_dir($path)) { 114 foreach (new \FilesystemIterator($path) as $item) { 115 static::delete($item->getPathname()); 116 } 117 118 if (!@rmdir($path)) { // @ is escalated to exception 119 throw new Nette\IOException(sprintf( 120 "Unable to delete directory '%s'. %s", 121 self::normalizePath($path), 122 Helpers::getLastError(), 123 )); 124 } 125 } 126 } 127 128 129 /** 130 * Renames or moves a file or a directory. Overwrites existing files and directories by default. 131 * @throws Nette\IOException on error occurred 132 * @throws Nette\InvalidStateException if $overwrite is set to false and destination already exists 133 */ 134 public static function rename(string $origin, string $target, bool $overwrite = true): void 135 { 136 if (!$overwrite && file_exists($target)) { 137 throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target))); 138 139 } elseif (!file_exists($origin)) { 140 throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin))); 141 142 } else { 143 static::createDir(dirname($target)); 144 if (realpath($origin) !== realpath($target)) { 145 static::delete($target); 146 } 147 148 if (!@rename($origin, $target)) { // @ is escalated to exception 149 throw new Nette\IOException(sprintf( 150 "Unable to rename file or directory '%s' to '%s'. %s", 151 self::normalizePath($origin), 152 self::normalizePath($target), 153 Helpers::getLastError(), 154 )); 155 } 156 } 157 } 158 159 160 /** 161 * Reads the content of a file. 162 * @throws Nette\IOException on error occurred 163 */ 164 public static function read(string $file): string 165 { 166 $content = @file_get_contents($file); // @ is escalated to exception 167 if ($content === false) { 168 throw new Nette\IOException(sprintf( 169 "Unable to read file '%s'. %s", 170 self::normalizePath($file), 171 Helpers::getLastError(), 172 )); 173 } 174 175 return $content; 176 } 177 178 179 /** 180 * Reads the file content line by line. Because it reads continuously as we iterate over the lines, 181 * it is possible to read files larger than the available memory. 182 * @return \Generator<int, string> 183 * @throws Nette\IOException on error occurred 184 */ 185 public static function readLines(string $file, bool $stripNewLines = true): \Generator 186 { 187 return (function ($f) use ($file, $stripNewLines) { 188 $counter = 0; 189 do { 190 $line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf( 191 "Unable to read file '%s'. %s", 192 self::normalizePath($file), 193 $error, 194 ))); 195 if ($line === false) { 196 fclose($f); 197 break; 198 } 199 if ($stripNewLines) { 200 $line = rtrim($line, "\r\n"); 201 } 202 203 yield $counter++ => $line; 204 205 } while (true); 206 })(static::open($file, 'r')); 207 } 208 209 210 /** 211 * Writes the string to a file. 212 * @throws Nette\IOException on error occurred 213 */ 214 public static function write(string $file, string $content, ?int $mode = 0666): void 215 { 216 static::createDir(dirname($file)); 217 if (@file_put_contents($file, $content) === false) { // @ is escalated to exception 218 throw new Nette\IOException(sprintf( 219 "Unable to write file '%s'. %s", 220 self::normalizePath($file), 221 Helpers::getLastError(), 222 )); 223 } 224 225 if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception 226 throw new Nette\IOException(sprintf( 227 "Unable to chmod file '%s' to mode %s. %s", 228 self::normalizePath($file), 229 decoct($mode), 230 Helpers::getLastError(), 231 )); 232 } 233 } 234 235 236 /** 237 * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`. 238 * Recursively traverses and sets permissions on the entire contents of the directory as well. 239 * @throws Nette\IOException on error occurred 240 */ 241 public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void 242 { 243 if (is_file($path)) { 244 if (!@chmod($path, $fileMode)) { // @ is escalated to exception 245 throw new Nette\IOException(sprintf( 246 "Unable to chmod file '%s' to mode %s. %s", 247 self::normalizePath($path), 248 decoct($fileMode), 249 Helpers::getLastError(), 250 )); 251 } 252 } elseif (is_dir($path)) { 253 foreach (new \FilesystemIterator($path) as $item) { 254 static::makeWritable($item->getPathname(), $dirMode, $fileMode); 255 } 256 257 if (!@chmod($path, $dirMode)) { // @ is escalated to exception 258 throw new Nette\IOException(sprintf( 259 "Unable to chmod directory '%s' to mode %s. %s", 260 self::normalizePath($path), 261 decoct($dirMode), 262 Helpers::getLastError(), 263 )); 264 } 265 } else { 266 throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path))); 267 } 268 } 269 270 271 /** 272 * Determines if the path is absolute. 273 */ 274 public static function isAbsolute(string $path): bool 275 { 276 return (bool) preg_match('#([a-z]:)?[/\\\\]|[a-z][a-z0-9+.-]*://#Ai', $path); 277 } 278 279 280 /** 281 * Normalizes `..` and `.` and directory separators in path. 282 */ 283 public static function normalizePath(string $path): string 284 { 285 $parts = $path === '' ? [] : preg_split('~[/\\\\]+~', $path); 286 $res = []; 287 foreach ($parts as $part) { 288 if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') { 289 array_pop($res); 290 } elseif ($part !== '.') { 291 $res[] = $part; 292 } 293 } 294 295 return $res === [''] 296 ? DIRECTORY_SEPARATOR 297 : implode(DIRECTORY_SEPARATOR, $res); 298 } 299 300 301 /** 302 * Joins all segments of the path and normalizes the result. 303 */ 304 public static function joinPaths(string ...$paths): string 305 { 306 return self::normalizePath(implode('/', $paths)); 307 } 308 309 310 /** 311 * Converts backslashes to slashes. 312 */ 313 public static function unixSlashes(string $path): string 314 { 315 return strtr($path, '\\', '/'); 316 } 317 318 319 /** 320 * Converts slashes to platform-specific directory separators. 321 */ 322 public static function platformSlashes(string $path): string 323 { 324 return DIRECTORY_SEPARATOR === '/' 325 ? strtr($path, '\\', '/') 326 : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol:// 327 } 328} 329