• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / utils / 21938400483

12 Feb 2026 08:03AM UTC coverage: 93.193% (-0.2%) from 93.429%
21938400483

push

github

dg
added CLAUDE.md

2081 of 2233 relevant lines covered (93.19%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.32
/src/Utils/FileSystem.php
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

8
declare(strict_types=1);
9

10
namespace Nette\Utils;
11

12
use Nette;
13
use 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;
14
use const DIRECTORY_SEPARATOR;
15

16

17
/**
18
 * File system tool.
19
 */
20
final 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 = 0o777): void
1✔
27
        {
28
                if (!is_dir($dir) && !@mkdir($dir, $mode, recursive: true) && !is_dir($dir)) { // @ - dir may already exist
1✔
29
                        throw new Nette\IOException(sprintf(
1✔
30
                                "Unable to create directory '%s' with mode %s. %s",
1✔
31
                                self::normalizePath($dir),
1✔
32
                                decoct($mode),
1✔
33
                                Helpers::getLastError(),
1✔
34
                        ));
35
                }
36
        }
1✔
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
1✔
45
        {
46
                if (stream_is_local($origin) && !file_exists($origin)) {
1✔
47
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
1✔
48

49
                } elseif (!$overwrite && file_exists($target)) {
1✔
50
                        throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
1✔
51

52
                } elseif (is_dir($origin)) {
1✔
53
                        static::createDir($target);
1✔
54
                        foreach (new \FilesystemIterator($target) as $item) {
1✔
55
                                \assert($item instanceof \SplFileInfo);
56
                                static::delete($item->getPathname());
1✔
57
                        }
58

59
                        foreach ($iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS), \RecursiveIteratorIterator::SELF_FIRST) as $item) {
1✔
60
                                if ($item->isDir()) {
1✔
61
                                        static::createDir($target . '/' . $iterator->getSubPathname());
×
62
                                } else {
63
                                        static::copy($item->getPathname(), $target . '/' . $iterator->getSubPathname());
1✔
64
                                }
65
                        }
66
                } else {
67
                        static::createDir(dirname($target));
1✔
68
                        if (@stream_copy_to_stream(static::open($origin, 'rb'), static::open($target, 'wb')) === false) { // @ is escalated to exception
1✔
69
                                throw new Nette\IOException(sprintf(
×
70
                                        "Unable to copy file '%s' to '%s'. %s",
×
71
                                        self::normalizePath($origin),
×
72
                                        self::normalizePath($target),
×
73
                                        Helpers::getLastError(),
×
74
                                ));
75
                        }
76
                }
77
        }
1✔
78

79

80
        /**
81
         * Opens file and returns resource.
82
         * @return resource
83
         * @throws Nette\IOException  on error occurred
84
         */
85
        public static function open(string $path, string $mode)
1✔
86
        {
87
                $f = @fopen($path, $mode); // @ is escalated to exception
1✔
88
                if (!$f) {
1✔
89
                        throw new Nette\IOException(sprintf(
1✔
90
                                "Unable to open file '%s'. %s",
1✔
91
                                self::normalizePath($path),
1✔
92
                                Helpers::getLastError(),
1✔
93
                        ));
94
                }
95
                return $f;
1✔
96
        }
97

98

99
        /**
100
         * Deletes a file or an entire directory if exists. If the directory is not empty, it deletes its contents first.
101
         * @throws Nette\IOException  on error occurred
102
         */
103
        public static function delete(string $path): void
1✔
104
        {
105
                if (is_file($path) || is_link($path)) {
1✔
106
                        $func = DIRECTORY_SEPARATOR === '\\' && is_dir($path) ? 'rmdir' : 'unlink';
1✔
107
                        if (!@$func($path)) { // @ is escalated to exception
1✔
108
                                throw new Nette\IOException(sprintf(
×
109
                                        "Unable to delete '%s'. %s",
×
110
                                        self::normalizePath($path),
×
111
                                        Helpers::getLastError(),
×
112
                                ));
113
                        }
114
                } elseif (is_dir($path)) {
1✔
115
                        foreach (new \FilesystemIterator($path) as $item) {
1✔
116
                                \assert($item instanceof \SplFileInfo);
117
                                static::delete($item->getPathname());
1✔
118
                        }
119

120
                        if (!@rmdir($path)) { // @ is escalated to exception
1✔
121
                                throw new Nette\IOException(sprintf(
×
122
                                        "Unable to delete directory '%s'. %s",
×
123
                                        self::normalizePath($path),
×
124
                                        Helpers::getLastError(),
×
125
                                ));
126
                        }
127
                }
128
        }
1✔
129

130

131
        /**
132
         * Renames or moves a file or a directory. Overwrites existing files and directories by default.
133
         * @throws Nette\IOException  on error occurred
134
         * @throws Nette\InvalidStateException  if $overwrite is set to false and destination already exists
135
         */
136
        public static function rename(string $origin, string $target, bool $overwrite = true): void
1✔
137
        {
138
                if (!$overwrite && file_exists($target)) {
1✔
139
                        throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
1✔
140

141
                } elseif (!file_exists($origin)) {
1✔
142
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
1✔
143

144
                } else {
145
                        static::createDir(dirname($target));
1✔
146
                        if (realpath($origin) !== realpath($target)) {
1✔
147
                                static::delete($target);
1✔
148
                        }
149

150
                        if (!@rename($origin, $target)) { // @ is escalated to exception
1✔
151
                                throw new Nette\IOException(sprintf(
×
152
                                        "Unable to rename file or directory '%s' to '%s'. %s",
×
153
                                        self::normalizePath($origin),
×
154
                                        self::normalizePath($target),
×
155
                                        Helpers::getLastError(),
×
156
                                ));
157
                        }
158
                }
159
        }
1✔
160

161

162
        /**
163
         * Reads the content of a file.
164
         * @throws Nette\IOException  on error occurred
165
         */
166
        public static function read(string $file): string
1✔
167
        {
168
                $content = @file_get_contents($file); // @ is escalated to exception
1✔
169
                if ($content === false) {
1✔
170
                        throw new Nette\IOException(sprintf(
1✔
171
                                "Unable to read file '%s'. %s",
1✔
172
                                self::normalizePath($file),
1✔
173
                                Helpers::getLastError(),
1✔
174
                        ));
175
                }
176

177
                return $content;
1✔
178
        }
179

180

181
        /**
182
         * Reads the file content line by line. Because it reads continuously as we iterate over the lines,
183
         * it is possible to read files larger than the available memory.
184
         * @return \Generator<int, string>
185
         * @throws Nette\IOException  on error occurred
186
         */
187
        public static function readLines(string $file, bool $stripNewLines = true): \Generator
1✔
188
        {
189
                return (function ($f) use ($file, $stripNewLines) {
1✔
190
                        $counter = 0;
1✔
191
                        do {
192
                                $line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf(
1✔
193
                                        "Unable to read file '%s'. %s",
194
                                        self::normalizePath($file),
195
                                        $error,
196
                                )));
1✔
197
                                if ($line === false) {
1✔
198
                                        fclose($f);
1✔
199
                                        break;
1✔
200
                                }
201
                                if ($stripNewLines) {
1✔
202
                                        $line = rtrim($line, "\r\n");
1✔
203
                                }
204

205
                                yield $counter++ => $line;
1✔
206

207
                        } while (true);
1✔
208
                })(static::open($file, 'r'));
1✔
209
        }
210

211

212
        /**
213
         * Writes the string to a file.
214
         * @throws Nette\IOException  on error occurred
215
         */
216
        public static function write(string $file, string $content, ?int $mode = 0o666): void
1✔
217
        {
218
                static::createDir(dirname($file));
1✔
219
                if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
1✔
220
                        throw new Nette\IOException(sprintf(
×
221
                                "Unable to write file '%s'. %s",
×
222
                                self::normalizePath($file),
×
223
                                Helpers::getLastError(),
×
224
                        ));
225
                }
226

227
                if ($mode !== null && !@chmod($file, $mode)) { // @ is escalated to exception
1✔
228
                        throw new Nette\IOException(sprintf(
×
229
                                "Unable to chmod file '%s' to mode %s. %s",
×
230
                                self::normalizePath($file),
×
231
                                decoct($mode),
×
232
                                Helpers::getLastError(),
×
233
                        ));
234
                }
235
        }
1✔
236

237

238
        /**
239
         * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
240
         * Recursively traverses and sets permissions on the entire contents of the directory as well.
241
         * @throws Nette\IOException  on error occurred
242
         */
243
        public static function makeWritable(string $path, int $dirMode = 0o777, int $fileMode = 0o666): void
1✔
244
        {
245
                if (is_file($path)) {
1✔
246
                        if (!@chmod($path, $fileMode)) { // @ is escalated to exception
1✔
247
                                throw new Nette\IOException(sprintf(
×
248
                                        "Unable to chmod file '%s' to mode %s. %s",
×
249
                                        self::normalizePath($path),
×
250
                                        decoct($fileMode),
×
251
                                        Helpers::getLastError(),
×
252
                                ));
253
                        }
254
                } elseif (is_dir($path)) {
1✔
255
                        foreach (new \FilesystemIterator($path) as $item) {
1✔
256
                                \assert($item instanceof \SplFileInfo);
257
                                static::makeWritable($item->getPathname(), $dirMode, $fileMode);
1✔
258
                        }
259

260
                        if (!@chmod($path, $dirMode)) { // @ is escalated to exception
1✔
261
                                throw new Nette\IOException(sprintf(
×
262
                                        "Unable to chmod directory '%s' to mode %s. %s",
×
263
                                        self::normalizePath($path),
×
264
                                        decoct($dirMode),
×
265
                                        Helpers::getLastError(),
×
266
                                ));
267
                        }
268
                } else {
269
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path)));
1✔
270
                }
271
        }
1✔
272

273

274
        /**
275
         * Determines if the path is absolute.
276
         */
277
        public static function isAbsolute(string $path): bool
1✔
278
        {
279
                return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
1✔
280
        }
281

282

283
        /**
284
         * Normalizes `..` and `.` and directory separators in path.
285
         */
286
        public static function normalizePath(string $path): string
1✔
287
        {
288
                $parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
1✔
289
                $res = [];
1✔
290
                foreach ($parts as $part) {
1✔
291
                        if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
1✔
292
                                array_pop($res);
1✔
293
                        } elseif ($part !== '.') {
1✔
294
                                $res[] = $part;
1✔
295
                        }
296
                }
297

298
                return $res === ['']
1✔
299
                        ? DIRECTORY_SEPARATOR
1✔
300
                        : implode(DIRECTORY_SEPARATOR, $res);
1✔
301
        }
302

303

304
        /**
305
         * Joins all segments of the path and normalizes the result.
306
         */
307
        public static function joinPaths(string ...$paths): string
1✔
308
        {
309
                return self::normalizePath(implode('/', $paths));
1✔
310
        }
311

312

313
        /**
314
         * 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.
315
         */
316
        public static function resolvePath(string $basePath, string $path): string
1✔
317
        {
318
                return match (true) {
319
                        self::isAbsolute($path) => self::platformSlashes($path),
1✔
320
                        $path === '' => self::platformSlashes($basePath),
1✔
321
                        default => self::joinPaths($basePath, $path),
1✔
322
                };
323
        }
324

325

326
        /**
327
         * Converts backslashes to slashes.
328
         */
329
        public static function unixSlashes(string $path): string
1✔
330
        {
331
                return strtr($path, '\\', '/');
1✔
332
        }
333

334

335
        /**
336
         * Converts slashes to platform-specific directory separators.
337
         */
338
        public static function platformSlashes(string $path): string
1✔
339
        {
340
                return DIRECTORY_SEPARATOR === '/'
1✔
341
                        ? strtr($path, '\\', '/')
1✔
342
                        : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
1✔
343
        }
344
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc