Files::deleteDirectory()   A
last analyzed

Complexity

Conditions 5
Paths 7

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5

Importance

Changes 0
Metric Value
eloc 13
dl 0
loc 24
ccs 14
cts 14
cp 1
rs 9.5222
c 0
b 0
f 0
cc 5
nc 7
nop 2
crap 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Files;
6
7
use Spiral\Files\Exception\FileNotFoundException;
8
use Spiral\Files\Exception\FilesException;
9
use Spiral\Files\Exception\WriteErrorException;
10
11
/**
12
 * Default abstraction for file management operations.
13
 */
14
final class Files implements FilesInterface
15
{
16
    /**
17
     * Default file mode for this manager.
18
     */
19
    public const DEFAULT_FILE_MODE = self::READONLY;
20
21
    /**
22
     * Files to be removed when component destructed.
23
     */
24
    private array $destructFiles = [];
25
26
    /**
27
     * FileManager constructor.
28
     */
29 562
    public function __construct()
30
    {
31 562
        \register_shutdown_function([$this, '__destruct']);
32
    }
33
34
    /**
35
     * @param bool $recursivePermissions Propagate permissions on created directories.
36
     */
37 497
    public function ensureDirectory(
38
        string $directory,
39
        ?int $mode = null,
40
        bool $recursivePermissions = true,
41
    ): bool {
42 497
        if (empty($mode)) {
43 8
            $mode = self::DEFAULT_FILE_MODE;
44
        }
45
46
        //Directories always executable
47 497
        $mode |= 0o111;
48 497
        if (\is_dir($directory)) {
49
            //Exists :(
50 488
            return $this->setPermissions($directory, $mode);
51
        }
52
53 391
        if (!$recursivePermissions) {
54 1
            return \mkdir($directory, $mode, true);
55
        }
56
57 390
        $directoryChain = [\basename($directory)];
58
59 390
        $baseDirectory = $directory;
60 390
        while (!\is_dir($baseDirectory = \dirname($baseDirectory))) {
61 372
            $directoryChain[] = \basename($baseDirectory);
62
        }
63
64 390
        foreach (\array_reverse($directoryChain) as $dir) {
65 390
            if (!\mkdir($baseDirectory = \sprintf('%s/%s', $baseDirectory, $dir))) {
66
                return false;
67
            }
68
69 390
            \chmod($baseDirectory, $mode);
70
        }
71
72 390
        return true;
73
    }
74
75 43
    public function read(string $filename): string
76
    {
77 43
        if (!$this->exists($filename)) {
78 1
            throw new FileNotFoundException($filename);
79
        }
80
81 42
        $result = \file_get_contents($filename);
82 42
        return $result === false
83
            ? throw new FilesException(\sprintf('Unable to read file `%s`.', $filename))
84 42
            : $result;
85
    }
86
87
    /**
88
     * @param bool $append To append data at the end of existed file.
89
     */
90 469
    public function write(
91
        string $filename,
92
        string $data,
93
        ?int $mode = null,
94
        bool $ensureDirectory = false,
95
        bool $append = false,
96
    ): bool {
97 469
        $mode ??= self::DEFAULT_FILE_MODE;
98
99
        try {
100 469
            if ($ensureDirectory) {
101 450
                $this->ensureDirectory(\dirname($filename), $mode);
102
            }
103
104 469
            if ($this->exists($filename)) {
105
                //Forcing mode for existed file
106 103
                $this->setPermissions($filename, $mode);
107
            }
108
109 469
            $result = \file_put_contents(
110 469
                $filename,
111 469
                $data,
112 469
                $append ? FILE_APPEND | LOCK_EX : LOCK_EX,
113 469
            );
114
115 468
            if ($result !== false) {
116
                //Forcing mode after file creation
117 468
                $this->setPermissions($filename, $mode);
118
            }
119 1
        } catch (\Exception $e) {
120 1
            throw new WriteErrorException($e->getMessage(), (int) $e->getCode(), $e);
121
        }
122
123 468
        return $result !== false;
124
    }
125
126 3
    public function append(
127
        string $filename,
128
        string $data,
129
        ?int $mode = null,
130
        bool $ensureDirectory = false,
131
    ): bool {
132 3
        return $this->write($filename, $data, $mode, $ensureDirectory, true);
133
    }
134
135 422
    public function delete(string $filename): bool
136
    {
137 422
        if ($this->exists($filename)) {
138 421
            $result = \unlink($filename);
139
140
            //Wiping out changes in local file cache
141 421
            \clearstatcache(false, $filename);
142
143 421
            return $result;
144
        }
145
146 1
        return false;
147
    }
148
149
    /**
150
     * @see http://stackoverflow.com/questions/3349753/delete-directory-with-files-in-it
151
     *
152
     * @throws FilesException
153
     */
154 414
    public function deleteDirectory(string $directory, bool $contentOnly = false): bool
155
    {
156 414
        if (!$this->isDirectory($directory)) {
157 1
            throw new FilesException(\sprintf('Undefined or invalid directory %s', $directory));
158
        }
159
160 414
        $files = new \RecursiveIteratorIterator(
161 414
            new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
162 414
            \RecursiveIteratorIterator::CHILD_FIRST,
163 414
        );
164
165 414
        foreach ($files as $file) {
166 391
            if ($file->isDir()) {
167 374
                \rmdir($file->getRealPath());
168
            } else {
169 381
                $this->delete($file->getRealPath());
170
            }
171
        }
172
173 414
        if (!$contentOnly) {
174 366
            return \rmdir($directory);
175
        }
176
177 49
        return true;
178
    }
179
180 2
    public function move(string $filename, string $destination): bool
181
    {
182 2
        if (!$this->exists($filename)) {
183 1
            throw new FileNotFoundException($filename);
184
        }
185
186 1
        return \rename($filename, $destination);
187
    }
188
189 9
    public function copy(string $filename, string $destination): bool
190
    {
191 9
        if (!$this->exists($filename)) {
192 1
            throw new FileNotFoundException($filename);
193
        }
194
195 8
        return \copy($filename, $destination);
196
    }
197
198 5
    public function touch(string $filename, ?int $mode = null): bool
199
    {
200 5
        if (!\touch($filename)) {
201
            return false;
202
        }
203
204 5
        return $this->setPermissions($filename, $mode ?? self::DEFAULT_FILE_MODE);
205
    }
206
207 539
    public function exists(string $filename): bool
208
    {
209 539
        return \file_exists($filename);
210
    }
211
212 2
    public function size(string $filename): int
213
    {
214 2
        $this->exists($filename) or throw new FileNotFoundException($filename);
215
216 1
        return (int) \filesize($filename);
217
    }
218
219 3
    public function extension(string $filename): string
220
    {
221 3
        return \strtolower(\pathinfo($filename, PATHINFO_EXTENSION));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, Spir...les\PATHINFO_EXTENSION) can also be of type array; however, parameter $string of strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

221
        return \strtolower(/** @scrutinizer ignore-type */ \pathinfo($filename, PATHINFO_EXTENSION));
Loading history...
222
    }
223
224 9
    public function md5(string $filename): string
225
    {
226 9
        if (!$this->exists($filename)) {
227 1
            throw new FileNotFoundException($filename);
228
        }
229
230 8
        $result = \md5_file($filename);
231 8
        $result === false and throw new FilesException(
232 8
            \sprintf('Unable to read md5 hash for `%s`.', $filename),
233 8
        );
234
235 8
        return $result;
236
    }
237
238 2
    public function time(string $filename): int
239
    {
240 2
        if (!$this->exists($filename)) {
241 1
            throw new FileNotFoundException($filename);
242
        }
243
244 1
        return \filemtime($filename) ?: throw new FilesException(
245 1
            \sprintf('Unable to read modification time for `%s`.', $filename),
246 1
        );
247
    }
248
249 418
    public function isDirectory(string $filename): bool
250
    {
251 418
        return \is_dir($filename);
252
    }
253
254 16
    public function isFile(string $filename): bool
255
    {
256 16
        return \is_file($filename);
257
    }
258
259 495
    public function getPermissions(string $filename): int
260
    {
261 495
        $this->exists($filename) or throw new FileNotFoundException($filename);
262 495
        $permission = \fileperms($filename);
263 495
        $permission === false and throw new FilesException(
264 495
            \sprintf('Unable to read permissions for `%s`.', $filename),
265 495
        );
266 495
        return $permission & 0777;
267
    }
268
269 495
    public function setPermissions(string $filename, int $mode): bool
270
    {
271 495
        if (\is_dir($filename)) {
272
            //Directories must always be executable (i.e. 664 for dir => 775)
273 488
            $mode |= 0111;
274
        }
275
276 495
        return $this->getPermissions($filename) === $mode || \chmod($filename, $mode);
277
    }
278
279 22
    public function getFiles(string $location, ?string $pattern = null): array
280
    {
281 22
        $result = [];
282 22
        foreach ($this->filesIterator($location, $pattern) as $filename) {
283 21
            if ($this->isDirectory($filename->getPathname())) {
284 17
                $result = \array_merge($result, $this->getFiles(((string) $filename) . DIRECTORY_SEPARATOR));
285
286 17
                continue;
287
            }
288
289 21
            $result[] = $this->normalizePath((string) $filename);
290
        }
291
292 22
        return $result;
293
    }
294
295 4
    public function tempFilename(string $extension = '', ?string $location = null): string
296
    {
297 4
        if (empty($location)) {
298 3
            $location = \sys_get_temp_dir();
299
        }
300
301 4
        $filename = \tempnam($location, 'spiral');
302 4
        $filename === false and throw new FilesException('Unable to create temporary file.');
303
304 4
        if (!empty($extension)) {
305 3
            $old = $filename;
306 3
            $filename = \sprintf('%s.%s', $filename, $extension);
307 3
            \rename($old, $filename);
308 3
            $this->destructFiles[] = $filename;
309
        }
310
311 4
        return $filename;
312
    }
313
314 72
    public function normalizePath(string $path, bool $asDirectory = false): string
315
    {
316 72
        $isUnc = \str_starts_with($path, '\\\\') || \str_starts_with($path, '//');
317 72
        if ($isUnc) {
318 1
            $leadingSlashes = \substr($path, 0, 2);
319 1
            $path = \substr($path, 2);
320
        }
321
322 72
        $path = \str_replace(['//', '\\'], '/', $path);
323
324
        //Potentially open links and ../ type directories?
325 72
        return ($isUnc ? $leadingSlashes : '') . \rtrim($path, '/') . ($asDirectory ? '/' : '');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $leadingSlashes does not seem to be defined for all execution paths leading up to this point.
Loading history...
326
    }
327
328
    /**
329
     * @link http://stackoverflow.com/questions/2637945/getting-relative-path-from-absolute-path-in-php
330
     */
331 16
    public function relativePath(string $path, string $from): string
332
    {
333 16
        $path = $this->normalizePath($path);
334 16
        $from = $this->normalizePath($from);
335
336 16
        $from = \explode('/', $from);
337 16
        $path = \explode('/', $path);
338 16
        $relative = $path;
339
340 16
        foreach ($from as $depth => $dir) {
341
            //Find first non-matching dir
342 16
            if ($dir === $path[$depth]) {
343
                //Ignore this directory
344 16
                \array_shift($relative);
345
            } else {
346
                //Get number of remaining dirs to $from
347 1
                $remaining = \count($from) - $depth;
348 1
                if ($remaining > 1) {
349
                    //Add traversals up to first matching directory
350 1
                    $padLength = (\count($relative) + $remaining - 1) * -1;
351 1
                    $relative = \array_pad($relative, $padLength, '..');
352 1
                    break;
353
                }
354 1
                $relative[0] = './' . $relative[0];
355
            }
356
        }
357
358 16
        return \implode('/', $relative);
359
    }
360
361
    /**
362
     * Destruct every temporary file.
363
     */
364 1
    public function __destruct()
365
    {
366 1
        foreach ($this->destructFiles as $filename) {
367 1
            $this->delete($filename);
368
        }
369
    }
370
371 22
    private function filesIterator(string $location, ?string $pattern = null): \GlobIterator
372
    {
373 22
        $pattern ??= '*';
374 22
        $regexp = \rtrim($location, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . \ltrim($pattern, DIRECTORY_SEPARATOR);
375
376 22
        return new \GlobIterator($regexp);
377
    }
378
}
379