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

nette / utils / 39638515326

21 Oct 2026 04:50PM UTC coverage: 92.495% (-0.06%) from 92.551%
39638515326

push

github

dg
added support for PHP 8.6

1 of 1 new or added line in 1 file covered. (100.0%)

114 existing lines in 17 files now uncovered.

1861 of 2012 relevant lines covered (92.5%)

0.92 hits per line

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

77.17
/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;
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 = 0777): 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
                                static::delete($item->getPathname());
1 ✔
54
                        }
55

56
                        foreach ($iterator = new \RecursiveIteratorIterator(
1 ✔
57
                                new \RecursiveDirectoryIterator($origin, \RecursiveDirectoryIterator::SKIP_DOTS),
1 ✔
58
                                \RecursiveIteratorIterator::SELF_FIRST,
1 ✔
59
                        ) as $item) {
60
                                if ($item->isDir()) {
1 ✔
UNCOV
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),
×
UNCOV
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)
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",
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
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",
UNCOV
110
                                        self::normalizePath($path),
×
111
                                        Helpers::getLastError(),
1 ✔
112
                                ));
113
                        }
114
                } elseif (is_dir($path)) {
1 ✔
115
                        foreach (new \FilesystemIterator($path) as $item) {
1 ✔
116
                                static::delete($item->getPathname());
1 ✔
117
                        }
118

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

129

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

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

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

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

160

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

176
                return $content;
1 ✔
177
        }
178

179

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

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

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

210

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

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

236

237
        /**
238
         * Sets file permissions to `$fileMode` or directory permissions to `$dirMode`.
239
         * Recursively traverses and sets permissions on the entire contents of the directory as well.
240
         * @throws Nette\IOException  on error occurred
241
         */
242
        public static function makeWritable(string $path, int $dirMode = 0777, int $fileMode = 0666): void
243
        {
244
                if (is_file($path)) {
1 ✔
245
                        if (!@chmod($path, $fileMode)) { // @ is escalated to exception
1 ✔
246
                                throw new Nette\IOException(sprintf(
×
247
                                        "Unable to chmod file '%s' to mode %s. %s",
248
                                        self::normalizePath($path),
×
UNCOV
249
                                        decoct($fileMode),
×
250
                                        Helpers::getLastError(),
1 ✔
251
                                ));
252
                        }
253
                } elseif (is_dir($path)) {
1 ✔
254
                        foreach (new \FilesystemIterator($path) as $item) {
1 ✔
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),
×
UNCOV
262
                                        decoct($dirMode),
×
263
                                        Helpers::getLastError(),
1 ✔
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
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
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
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
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
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
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 TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc