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

nette / utils / 22290136219

23 Feb 2026 01:47AM UTC coverage: 93.125% (-0.003%) from 93.128%
22290136219

push

github

dg
added CLAUDE.md

2086 of 2240 relevant lines covered (93.13%)

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 declare(strict_types=1);
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
namespace Nette\Utils;
9

10
use Nette;
11
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;
12
use const DIRECTORY_SEPARATOR;
13

14

15
/**
16
 * File system tool.
17
 */
18
final class FileSystem
19
{
20
        /**
21
         * Creates a directory if it does not exist, including parent directories.
22
         * @throws Nette\IOException  on error occurred
23
         */
24
        public static function createDir(string $dir, int $mode = 0o777): void
1✔
25
        {
26
                if (!is_dir($dir) && !@mkdir($dir, $mode, recursive: true) && !is_dir($dir)) { // @ - dir may already exist
1✔
27
                        throw new Nette\IOException(sprintf(
1✔
28
                                "Unable to create directory '%s' with mode %s. %s",
1✔
29
                                self::normalizePath($dir),
1✔
30
                                decoct($mode),
1✔
31
                                Helpers::getLastError(),
1✔
32
                        ));
33
                }
34
        }
1✔
35

36

37
        /**
38
         * Copies a file or an entire directory. Overwrites existing files and directories by default.
39
         * @throws Nette\IOException  on error occurred
40
         * @throws Nette\InvalidStateException  if $overwrite is set to false and destination already exists
41
         */
42
        public static function copy(string $origin, string $target, bool $overwrite = true): void
1✔
43
        {
44
                if (stream_is_local($origin) && !file_exists($origin)) {
1✔
45
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($origin)));
1✔
46

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

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

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

77

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

96

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

118
                        if (!@rmdir($path)) { // @ is escalated to exception
1✔
119
                                throw new Nette\IOException(sprintf(
×
120
                                        "Unable to delete directory '%s'. %s",
×
121
                                        self::normalizePath($path),
×
122
                                        Helpers::getLastError(),
×
123
                                ));
124
                        }
125
                }
126
        }
1✔
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
1✔
135
        {
136
                if (!$overwrite && file_exists($target)) {
1✔
137
                        throw new Nette\InvalidStateException(sprintf("File or directory '%s' already exists.", self::normalizePath($target)));
1✔
138

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

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

148
                        if (!@rename($origin, $target)) { // @ is escalated to exception
1✔
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
        }
1✔
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
1✔
165
        {
166
                $content = @file_get_contents($file); // @ is escalated to exception
1✔
167
                if ($content === false) {
1✔
168
                        throw new Nette\IOException(sprintf(
1✔
169
                                "Unable to read file '%s'. %s",
1✔
170
                                self::normalizePath($file),
1✔
171
                                Helpers::getLastError(),
1✔
172
                        ));
173
                }
174

175
                return $content;
1✔
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
1✔
186
        {
187
                return (function ($f) use ($file, $stripNewLines) {
1✔
188
                        $counter = 0;
1✔
189
                        do {
190
                                $line = Callback::invokeSafe('fgets', [$f], fn($error) => throw new Nette\IOException(sprintf(
1✔
191
                                        "Unable to read file '%s'. %s",
192
                                        self::normalizePath($file),
193
                                        $error,
194
                                )));
1✔
195
                                if ($line === false) {
1✔
196
                                        fclose($f);
1✔
197
                                        break;
1✔
198
                                }
199
                                if ($stripNewLines) {
1✔
200
                                        $line = rtrim($line, "\r\n");
1✔
201
                                }
202

203
                                yield $counter++ => $line;
1✔
204

205
                        } while (true);
1✔
206
                })(static::open($file, 'r'));
1✔
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 = 0o666): void
1✔
215
        {
216
                static::createDir(dirname($file));
1✔
217
                if (@file_put_contents($file, $content) === false) { // @ is escalated to exception
1✔
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
1✔
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
        }
1✔
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 = 0o777, int $fileMode = 0o666): void
1✔
242
        {
243
                if (is_file($path)) {
1✔
244
                        if (!@chmod($path, $fileMode)) { // @ is escalated to exception
1✔
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)) {
1✔
253
                        foreach (new \FilesystemIterator($path) as $item) {
1✔
254
                                \assert($item instanceof \SplFileInfo);
255
                                static::makeWritable($item->getPathname(), $dirMode, $fileMode);
1✔
256
                        }
257

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

271

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

280

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

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

301

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

310

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

323

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

332

333
        /**
334
         * Converts slashes to platform-specific directory separators.
335
         */
336
        public static function platformSlashes(string $path): string
1✔
337
        {
338
                return DIRECTORY_SEPARATOR === '/'
1✔
339
                        ? strtr($path, '\\', '/')
1✔
340
                        : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
1✔
341
        }
342
}
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