Passed
Pull Request — master (#22)
by Sergei
25:20 queued 10:23
created

FileHelper::matchPathname()   C

Complexity

Conditions 12
Paths 72

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 12.7571

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 12
eloc 22
c 1
b 0
f 0
nc 72
nop 5
dl 0
loc 44
ccs 19
cts 23
cp 0.8261
crap 12.7571
rs 6.9666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Files;
6
7
use Exception;
8
use FilesystemIterator;
9
use InvalidArgumentException;
10
use RecursiveIteratorIterator;
11
use RecursiveDirectoryIterator;
12
use RuntimeException;
13
14
/**
15
 * FileHelper provides useful methods to manage files and directories
16
 */
17
class FileHelper
18
{
19
    /**
20
     * Creates a new directory.
21
     *
22
     * This method is similar to the PHP `mkdir()` function except that it uses `chmod()` to set the permission of the
23
     * created directory in order to avoid the impact of the `umask` setting.
24
     *
25
     * @param string $path path of the directory to be created.
26
     * @param int $mode the permission to be set for the created directory.
27
     *
28
     * @return bool whether the directory is created successfully.
29
     */
30
    public static function createDirectory(string $path, int $mode = 0775): bool
31
    {
32
        $path = static::normalizePath($path);
33
34
        try {
35
            if (!mkdir($path, $mode, true) && !is_dir($path)) {
36
                return false;
37
            }
38
        } catch (Exception $e) {
39
            if (!is_dir($path)) {
40
                throw new RuntimeException(
41
                    "Failed to create directory \"$path\": " . $e->getMessage(),
42
                    $e->getCode(),
43
                    $e
44
                );
45
            }
46
        }
47
48
        return static::chmod($path, $mode);
49
    }
50
51
    /**
52
     * Set permissions directory.
53
     *
54
     * @param string $path
55
     * @param integer $mode
56 36
     *
57
     * @return boolean|null
58 36
     *
59
     * @throws RuntimeException
60
     */
61 36
    private static function chmod(string $path, int $mode): ?bool
62 36
    {
63
        try {
64 2
            return chmod($path, $mode);
65 2
        } catch (Exception $e) {
66 1
            throw new RuntimeException(
67 1
                "Failed to change permissions for directory \"$path\": " . $e->getMessage(),
68 1
                $e->getCode(),
69
                $e
70
            );
71
        }
72
    }
73
74 36
    /**
75
     * Normalizes a file/directory path.
76
     *
77
     * The normalization does the following work:
78
     *
79
     * - Convert all directory separators into `/` (e.g. "\a/b\c" becomes "/a/b/c")
80
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
81
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
82
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
83
     *
84
     * @param string $path the file/directory path to be normalized
85
     *
86
     * @return string the normalized file/directory path
87 36
     */
88
    public static function normalizePath(string $path): string
89
    {
90 36
        $isWindowsShare = strpos($path, '\\\\') === 0;
91
92
        if ($isWindowsShare) {
93
            $path = substr($path, 2);
94
        }
95
96
        $path = rtrim(strtr($path, '/\\', '//'), '/');
97
98
        if (strpos('/' . $path, '/.') === false && strpos($path, '//') === false) {
99
            return $isWindowsShare ? "\\\\$path" : $path;
100
        }
101
102
        $parts = [];
103
104
        foreach (explode('/', $path) as $part) {
105
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
106
                array_pop($parts);
107
            } elseif ($part !== '.' && ($part !== '' || empty($parts))) {
108
                $parts[] = $part;
109
            }
110
        }
111
112
        $path = implode('/', $parts);
113
114 36
        if ($isWindowsShare) {
115
            $path = '\\\\' . $path;
116 36
        }
117
118 36
        return $path === '' ? '.' : $path;
119 1
    }
120
121
    /**
122 36
     * Removes a directory (and all its content) recursively.
123
     *
124 36
     * @param string $directory the directory to be deleted recursively.
125 36
     * @param array $options options for directory remove ({@see clearDirectory()}).
126
     *
127
     * @return void
128 1
     */
129
    public static function removeDirectory(string $directory, array $options = []): void
130 1
    {
131 1
        try {
132 1
            static::clearDirectory($directory, $options);
133 1
        } catch (InvalidArgumentException $e) {
134 1
            return;
135
        }
136
137
        if (is_link($directory)) {
138 1
            self::unlink($directory);
139
        } else {
140 1
            rmdir($directory);
141 1
        }
142
    }
143
144 1
    /**
145
     * Clear all directory content.
146
     *
147
     * @param string $directory the directory to be cleared.
148
     * @param array $options options for directory clear . Valid options are:
149
     *
150
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
151
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
152
     *   Only symlink would be removed in that default case.
153
     *
154
     * @return void
155 36
     *
156
     * @throws InvalidArgumentException if unable to open directory
157
     */
158 36
    public static function clearDirectory(string $directory, array $options = []): void
159 1
    {
160 1
        $handle = static::openDirectory($directory);
161
        if (!empty($options['traverseSymlinks']) || !is_link($directory)) {
162
            while (($file = readdir($handle)) !== false) {
163 36
                if ($file === '.' || $file === '..') {
164 2
                    continue;
165
                }
166 36
                $path = $directory . '/' . $file;
167
                if (is_dir($path)) {
168 36
                    self::removeDirectory($path, $options);
169
                } else {
170
                    self::unlink($path);
171
                }
172
            }
173
            closedir($handle);
174
        }
175
    }
176
177
    /**
178
     * Removes a file or symlink in a cross-platform way.
179
     * @param string $path
180
     * @return bool
181
     */
182
    public static function unlink(string $path): bool
183
    {
184 36
        $isWindows = DIRECTORY_SEPARATOR === '\\';
185
186 36
        if (!$isWindows) {
187 36
            return unlink($path);
188 36
        }
189 36
190 36
        if (is_link($path) && is_dir($path)) {
191
            return rmdir($path);
192 24
        }
193 24
194 24
        if (!is_writable($path)) {
195
            chmod($path, 0777);
196 16
        }
197
198
        return unlink($path);
199 36
    }
200
201 36
    /**
202
     * Tells whether the path is a empty directory
203
     * @param string $path
204
     * @return bool
205
     */
206
    public static function isEmptyDirectory(string $path): bool
207
    {
208 17
        if (!is_dir($path)) {
209
            return false;
210 17
        }
211
212 17
        return !(new FilesystemIterator($path))->valid();
213 17
    }
214
215
    /**
216
     * Copies a whole directory as another one.
217
     *
218
     * The files and sub-directories will also be copied over.
219
     *
220
     * @param string $source the source directory.
221
     * @param string $destination the destination directory.
222
     * @param array $options options for directory copy. Valid options are:
223
     *
224
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
225
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment
226
     *   setting.
227
     * - filter: PathMatcherInterface
228
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
229
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. If the callback
230
     *   returns false, the copy operation for the sub-directory or file will be cancelled. The signature of the
231
     *   callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to be copied from,
232 1
     *   while `$to` is the copy target.
233
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
234 1
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file
235 1
     *   copied from, while `$to` is the copy target.
236
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating
237
     *   directories that do not contain files. This affects directories that do not contain files initially as well as
238 1
     *   directories that do not contain files at the target destination because files have been filtered via `only` or
239
     *   `except`. Defaults to true.
240
     *
241
     * @return void
242
     * @throws Exception
243
     *
244
     * @throws InvalidArgumentException if unable to open directory
245
     */
246
    public static function copyDirectory(string $source, string $destination, array $options = []): void
247
    {
248
        $source = static::normalizePath($source);
249
        $destination = static::normalizePath($destination);
250
251
        static::assertNotSelfDirectory($source, $destination);
252
253
        $destinationExists = static::setDestination($destination, $options);
254
255
        $handle = static::openDirectory($source);
256
257
        $options = static::setBasePath($source, $options);
258
259
        while (($file = readdir($handle)) !== false) {
260
            if ($file === '.' || $file === '..') {
261
                continue;
262
            }
263
264
            $from = $source . '/' . $file;
265
            $to = $destination . '/' . $file;
266
267
            if (!isset($options['filter']) || $options['filter']->match($from)) {
268
                if (is_file($from)) {
269
                    if (!$destinationExists) {
270
                        static::createDirectory($destination, $options['dirMode'] ?? 0775);
271
                        $destinationExists = true;
272
                    }
273
                    copy($from, $to);
274
                    if (isset($options['fileMode'])) {
275
                        static::chmod($to, $options['fileMode']);
276
                    }
277
                } elseif (!isset($options['recursive']) || $options['recursive']) {
278
                    static::copyDirectory($from, $to, $options);
279
                }
280
            }
281
        }
282
283
        closedir($handle);
284
    }
285
286
    /**
287
     * Check copy it self directory.
288
     *
289
     * @param string $source
290
     * @param string $destination
291 12
     *
292
     * @throws InvalidArgumentException
293 12
     */
294 12
    private static function assertNotSelfDirectory(string $source, string $destination): void
295
    {
296 12
        if ($source === $destination || strpos($destination, $source . '/') === 0) {
297
            throw new InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
298 10
        }
299
    }
300 10
301
    /**
302 9
     * Open directory handle.
303
     *
304 9
     * @param string $directory
305 9
     *
306 9
     * @return resource
307
     *
308
     * @throws InvalidArgumentException
309 7
     */
310 7
    private static function openDirectory(string $directory)
311
    {
312 7
        $handle = @opendir($directory);
313 7
314 7
        if ($handle === false) {
315 2
            throw new InvalidArgumentException("Unable to open directory: $directory");
316 2
        }
317
318 7
        return $handle;
319 7
    }
320 7
321
    /**
322 6
     * Set base path directory.
323 5
     *
324
     * @param string $source
325
     * @param array $options
326
     *
327
     * @return array
328 9
     */
329 9
    private static function setBasePath(string $source, array $options): array
330
    {
331
        if (!isset($options['basePath'])) {
332
            // this should be done only once
333
            $options['basePath'] = realpath($source);
334
        }
335
336
        return $options;
337
    }
338
339 12
    /**
340
     * Set destination directory.
341 12
     *
342 2
     * @param string $destination
343
     * @param array $options
344 10
     *
345
     * @return bool
346
     */
347
    private static function setDestination(string $destination, array $options): bool
348
    {
349
        $destinationExists = is_dir($destination);
350
351
        if (!$destinationExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
352
            static::createDirectory($destination, $options['dirMode'] ?? 0775);
353
            $destinationExists = true;
354 36
        }
355
356 36
        return $destinationExists;
357
    }
358 36
359 3
    /**
360
     * Returns the last modification time for the given path.
361
     *
362 36
     * If the path is a directory, any nested files/directories will be checked as well.
363
     *
364
     * @param string $path the directory to be checked
365
     *
366
     * @return int Unix timestamp representing the last modification time
367
     */
368
    public static function lastModifiedTime(string $path): int
369
    {
370
        if (is_file($path)) {
371
            return filemtime($path);
372
        }
373 9
374
        $times = [filemtime($path)];
375 9
376
        $iterator = new RecursiveIteratorIterator(
377 9
            new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS),
378 9
            RecursiveIteratorIterator::SELF_FIRST
379
        );
380
381 9
        foreach ($iterator as $p => $info) {
382
            $times[] = filemtime($p);
383
        }
384
385
        return max($times);
386
    }
387
}
388