friendship ended with social-app. php is my new best friend
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;
13use function array_pop, chmod, decoct, dirname, end, fclose, file_exists, file_get_contents, file_put_contents, fopen, implode, is_dir, is_file, is_link, mkdir, preg_match, preg_split, realpath, rename, rmdir, rtrim, sprintf, str_replace, stream_copy_to_stream, stream_is_local, strtr;
14use const DIRECTORY_SEPARATOR;
15
16
17/**
18 * File system tool.
19 */
20final class FileSystem
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, recursive: 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 * Resolves a path against a base path. If the path is absolute, returns it directly, if it's relative, joins it with the base path.
312 */
313 public static function resolvePath(string $basePath, string $path): string
314 {
315 return match (true) {
316 self::isAbsolute($path) => self::platformSlashes($path),
317 $path === '' => self::platformSlashes($basePath),
318 default => self::joinPaths($basePath, $path),
319 };
320 }
321
322
323 /**
324 * Converts backslashes to slashes.
325 */
326 public static function unixSlashes(string $path): string
327 {
328 return strtr($path, '\\', '/');
329 }
330
331
332 /**
333 * Converts slashes to platform-specific directory separators.
334 */
335 public static function platformSlashes(string $path): string
336 {
337 return DIRECTORY_SEPARATOR === '/'
338 ? strtr($path, '\\', '/')
339 : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
340 }
341}