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

nette / utils / 38323312841

12 Oct 2026 11:39PM UTC coverage: 93.397% (-0.001%) from 93.398%
38323312841

push

github

dg
added upgrading.md

2136 of 2287 relevant lines covered (93.4%)

0.93 hits per line

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

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

80

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

99

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

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

131

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

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

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

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

162

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

178
                return $content;
1 ✔
179
        }
180

181

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

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

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

212

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

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

238

239
        /**
240
         * Writes the string to a file atomically: the content is written to a temporary file, which then replaces
241
         * the target, so a concurrent reader never sees the file partially written or truncated.
242
         * Creates the parent directory if it does not exist. Pass null as $mode to skip chmod.
243
         * @throws Nette\IOException  on error occurred
244
         */
245
        public static function writeAtomic(string $file, string $content, ?int $mode = 0o666): void
246
        {
247
                $file = realpath($file) ?: $file; // writes through a symlink to its target, as write() does
1 ✔
248
                $tmp = $file . '.' . uniqid(more_entropy: true) . '.tmp';
1 ✔
249
                try {
250
                        static::write($tmp, $content, $mode);
1 ✔
251
                        // plain rename() is atomic; static::rename() must not be used here, it deletes the target first
252
                        for ($i = 0; !@rename($tmp, $file); $i++) { // @ is escalated to exception
1 ✔
253
                                if (!Helpers::IsWindows || $i >= 20) {
1 ✔
254
                                        throw new Nette\IOException(sprintf(
1 ✔
255
                                                "Unable to write file '%s'. %s",
256
                                                self::normalizePath($file),
1 ✔
257
                                                Helpers::getLastError(),
1 ✔
258
                                        ));
259
                                }
260
                                usleep(5_000); // on Windows, rename fails while the target is open in another process
×
261
                        }
262
                } catch (\Throwable $e) {
1 ✔
263
                        @unlink($tmp); // @ file may not exist
1 ✔
264
                        throw $e;
1 ✔
265
                }
266
        }
1 ✔
267

268

269
        /**
270
         * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
271
         * Recursively traverses and sets permissions on the entire contents of the directory as well.
272
         * @throws Nette\IOException  on error occurred
273
         */
274
        public static function makeWritable(string $path, int $dirMode = 0o777, int $fileMode = 0o666): void
275
        {
276
                if (is_file($path)) {
1 ✔
277
                        if (!@chmod($path, $fileMode)) { // @ is escalated to exception
1 ✔
278
                                throw new Nette\IOException(sprintf(
×
279
                                        "Unable to chmod file '%s' to mode %s. %s",
280
                                        self::normalizePath($path),
×
281
                                        decoct($fileMode),
×
282
                                        Helpers::getLastError(),
×
283
                                ));
284
                        }
285
                } elseif (is_dir($path)) {
1 ✔
286
                        foreach (new \FilesystemIterator($path) as $item) {
1 ✔
287
                                assert($item instanceof \SplFileInfo);
288
                                static::makeWritable($item->getPathname(), $dirMode, $fileMode);
1 ✔
289
                        }
290

291
                        if (!@chmod($path, $dirMode)) { // @ is escalated to exception
1 ✔
292
                                throw new Nette\IOException(sprintf(
×
293
                                        "Unable to chmod directory '%s' to mode %s. %s",
294
                                        self::normalizePath($path),
×
295
                                        decoct($dirMode),
×
296
                                        Helpers::getLastError(),
×
297
                                ));
298
                        }
299
                } else {
300
                        throw new Nette\IOException(sprintf("File or directory '%s' not found.", self::normalizePath($path)));
1 ✔
301
                }
302
        }
1 ✔
303

304

305
        /**
306
         * Determines if the path is absolute.
307
         */
308
        public static function isAbsolute(string $path): bool
309
        {
310
                return (bool) preg_match('#([a-z]:)?[/\\\]|[a-z][a-z0-9+.-]*://#Ai', $path);
1 ✔
311
        }
312

313

314
        /**
315
         * Determines whether the string is a valid cross-platform filename without any path information.
316
         */
317
        public static function isValidFilename(string $name): bool
318
        {
319
                [$stem] = explode('.', $name, 2);
1 ✔
320
                return $name !== '' && $name !== '.' && $name !== '..'
1 ✔
321
                        && !preg_match('#[\x00-\x1F<>:"|?*\\\/]#', $name) // control and reserved characters
1 ✔
322
                        && !str_ends_with($name, '.') && !str_ends_with($name, ' ') // trailing dots/spaces
1 ✔
323
                        && !preg_match('#^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$#i', $stem); // Windows reserved device names
1 ✔
324
        }
325

326

327
        /**
328
         * Normalizes `..` and `.` and directory separators in path; a scheme like `phar://` is kept.
329
         */
330
        public static function normalizePath(string $path): string
331
        {
332
                $scheme = preg_match('#^[a-z][a-z0-9+.-]*://#i', $path, $m) ? $m[0] : '';
1 ✔
333
                $path = substr($path, strlen($scheme));
1 ✔
334
                $parts = $path === '' ? [] : preg_split('~[/\\\]+~', $path);
1 ✔
335
                $res = [];
1 ✔
336
                foreach ($parts as $part) {
1 ✔
337
                        if ($part === '..' && $res && end($res) !== '..' && end($res) !== '') {
1 ✔
338
                                array_pop($res);
1 ✔
339
                        } elseif ($part !== '.') {
1 ✔
340
                                $res[] = $part;
1 ✔
341
                        }
342
                }
343

344
                return $scheme . ($res === ['']
1 ✔
345
                        ? DIRECTORY_SEPARATOR
1 ✔
346
                        : implode(DIRECTORY_SEPARATOR, $res));
1 ✔
347
        }
348

349

350
        /**
351
         * Joins all segments of the path and normalizes the result.
352
         */
353
        public static function joinPaths(string ...$paths): string
354
        {
355
                return self::normalizePath(implode('/', $paths));
1 ✔
356
        }
357

358

359
        /**
360
         * 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.
361
         */
362
        public static function resolvePath(string $basePath, string $path): string
363
        {
364
                return match (true) {
365
                        self::isAbsolute($path) => self::platformSlashes($path),
1 ✔
366
                        $path === '' => self::platformSlashes($basePath),
1 ✔
367
                        default => self::joinPaths($basePath, $path),
1 ✔
368
                };
369
        }
370

371

372
        /**
373
         * Converts backslashes to slashes.
374
         */
375
        public static function unixSlashes(string $path): string
376
        {
377
                return strtr($path, '\\', '/');
1 ✔
378
        }
379

380

381
        /**
382
         * Converts slashes to platform-specific directory separators.
383
         */
384
        public static function platformSlashes(string $path): string
385
        {
386
                return DIRECTORY_SEPARATOR === '/'
1 ✔
387
                        ? strtr($path, '\\', '/')
1 ✔
388
                        : str_replace(':\\\\', '://', strtr($path, '/', '\\')); // protocol://
1 ✔
389
        }
390
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc