Passed
Push — master ( e2b43f...ae602f )
by butschster
04:24 queued 18s
created

Files::isFile()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 478
    public function __construct()
30
    {
31 478
        \register_shutdown_function([$this, '__destruct']);
32
    }
33
34
    /**
35
     * Destruct every temporary file.
36
     */
37 1
    public function __destruct()
38
    {
39 1
        foreach ($this->destructFiles as $filename) {
40 1
            $this->delete($filename);
41
        }
42
    }
43
44
    /**
45
     * @param bool $recursivePermissions Propagate permissions on created directories.
46
     */
47 429
    public function ensureDirectory(
48
        string $directory,
49
        int $mode = null,
50
        bool $recursivePermissions = true
51
    ): bool {
52 429
        if (empty($mode)) {
53 19
            $mode = self::DEFAULT_FILE_MODE;
54
        }
55
56
        //Directories always executable
57 429
        $mode |= 0o111;
58 429
        if (\is_dir($directory)) {
59
            //Exists :(
60 425
            return $this->setPermissions($directory, $mode);
61
        }
62
63 324
        if (!$recursivePermissions) {
64 1
            return \mkdir($directory, $mode, true);
65
        }
66
67 323
        $directoryChain = [\basename($directory)];
68
69 323
        $baseDirectory = $directory;
70 323
        while (!\is_dir($baseDirectory = \dirname($baseDirectory))) {
71 305
            $directoryChain[] = \basename($baseDirectory);
72
        }
73
74 323
        foreach (\array_reverse($directoryChain) as $dir) {
75 323
            if (!mkdir($baseDirectory = \sprintf('%s/%s', $baseDirectory, $dir))) {
76
                return false;
77
            }
78
79 323
            \chmod($baseDirectory, $mode);
80
        }
81
82 323
        return true;
83
    }
84
85 43
    public function read(string $filename): string
86
    {
87 43
        if (!$this->exists($filename)) {
88 1
            throw new FileNotFoundException($filename);
89
        }
90
91 42
        return \file_get_contents($filename);
92
    }
93
94
    /**
95
     * @param bool $append To append data at the end of existed file.
96
     */
97 401
    public function write(
98
        string $filename,
99
        string $data,
100
        int $mode = null,
101
        bool $ensureDirectory = false,
102
        bool $append = false
103
    ): bool {
104 401
        $mode ??= self::DEFAULT_FILE_MODE;
105
106
        try {
107 401
            if ($ensureDirectory) {
108 382
                $this->ensureDirectory(\dirname($filename), $mode);
109
            }
110
111 401
            if ($this->exists($filename)) {
112
                //Forcing mode for existed file
113 96
                $this->setPermissions($filename, $mode);
114
            }
115
116 401
            $result = \file_put_contents(
117 401
                $filename,
118 401
                $data,
119 401
                $append ? FILE_APPEND | LOCK_EX : LOCK_EX
120 401
            );
121
122 400
            if ($result !== false) {
123
                //Forcing mode after file creation
124 400
                $this->setPermissions($filename, $mode);
125
            }
126 1
        } catch (\Exception $e) {
127 1
            throw new WriteErrorException($e->getMessage(), (int) $e->getCode(), $e);
128
        }
129
130 400
        return $result !== false;
131
    }
132
133 3
    public function append(
134
        string $filename,
135
        string $data,
136
        int $mode = null,
137
        bool $ensureDirectory = false
138
    ): bool {
139 3
        return $this->write($filename, $data, $mode, $ensureDirectory, true);
140
    }
141
142 355
    public function delete(string $filename): bool
143
    {
144 355
        if ($this->exists($filename)) {
145 354
            $result = \unlink($filename);
146
147
            //Wiping out changes in local file cache
148 354
            \clearstatcache(false, $filename);
149
150 354
            return $result;
151
        }
152
153 1
        return false;
154
    }
155
156
    /**
157
     * @see http://stackoverflow.com/questions/3349753/delete-directory-with-files-in-it
158
     *
159
     * @throws FilesException
160
     */
161 348
    public function deleteDirectory(string $directory, bool $contentOnly = false): bool
162
    {
163 348
        if (!$this->isDirectory($directory)) {
164 1
            throw new FilesException(\sprintf('Undefined or invalid directory %s', $directory));
165
        }
166
167 348
        $files = new \RecursiveIteratorIterator(
168 348
            new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
169 348
            \RecursiveIteratorIterator::CHILD_FIRST
170 348
        );
171
172 348
        foreach ($files as $file) {
173 325
            if ($file->isDir()) {
174 308
                \rmdir($file->getRealPath());
175
            } else {
176 315
                $this->delete($file->getRealPath());
177
            }
178
        }
179
180 348
        if (!$contentOnly) {
181 300
            return \rmdir($directory);
182
        }
183
184 49
        return true;
185
    }
186
187 2
    public function move(string $filename, string $destination): bool
188
    {
189 2
        if (!$this->exists($filename)) {
190 1
            throw new FileNotFoundException($filename);
191
        }
192
193 1
        return \rename($filename, $destination);
194
    }
195
196 9
    public function copy(string $filename, string $destination): bool
197
    {
198 9
        if (!$this->exists($filename)) {
199 1
            throw new FileNotFoundException($filename);
200
        }
201
202 8
        return \copy($filename, $destination);
203
    }
204
205 5
    public function touch(string $filename, int $mode = null): bool
206
    {
207 5
        if (!\touch($filename)) {
208
            return false;
209
        }
210
211 5
        return $this->setPermissions($filename, $mode ?? self::DEFAULT_FILE_MODE);
212
    }
213
214 456
    public function exists(string $filename): bool
215
    {
216 456
        return \file_exists($filename);
217
    }
218
219 2
    public function size(string $filename): int
220
    {
221 2
        if (!$this->exists($filename)) {
222 1
            throw new FileNotFoundException($filename);
223
        }
224
225 1
        return \filesize($filename);
226
    }
227
228 3
    public function extension(string $filename): string
229
    {
230 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

230
        return \strtolower(/** @scrutinizer ignore-type */ \pathinfo($filename, PATHINFO_EXTENSION));
Loading history...
231
    }
232
233 9
    public function md5(string $filename): string
234
    {
235 9
        if (!$this->exists($filename)) {
236 1
            throw new FileNotFoundException($filename);
237
        }
238
239 8
        return \md5_file($filename);
240
    }
241
242 2
    public function time(string $filename): int
243
    {
244 2
        if (!$this->exists($filename)) {
245 1
            throw new FileNotFoundException($filename);
246
        }
247
248 1
        return \filemtime($filename);
249
    }
250
251 351
    public function isDirectory(string $filename): bool
252
    {
253 351
        return \is_dir($filename);
254
    }
255
256 16
    public function isFile(string $filename): bool
257
    {
258 16
        return \is_file($filename);
259
    }
260
261 427
    public function getPermissions(string $filename): int
262
    {
263 427
        if (!$this->exists($filename)) {
264 1
            throw new FileNotFoundException($filename);
265
        }
266
267 427
        return \fileperms($filename) & 0777;
268
    }
269
270 427
    public function setPermissions(string $filename, int $mode): bool
271
    {
272 427
        if (\is_dir($filename)) {
273
            //Directories must always be executable (i.e. 664 for dir => 775)
274 425
            $mode |= 0111;
275
        }
276
277 427
        return $this->getPermissions($filename) === $mode || \chmod($filename, $mode);
278
    }
279
280 21
    public function getFiles(string $location, string $pattern = null): array
281
    {
282 21
        $result = [];
283 21
        foreach ($this->filesIterator($location, $pattern) as $filename) {
284 20
            if ($this->isDirectory($filename->getPathname())) {
285 17
                $result = \array_merge($result, $this->getFiles($filename . DIRECTORY_SEPARATOR));
286
287 17
                continue;
288
            }
289
290 20
            $result[] = $this->normalizePath((string)$filename);
291
        }
292
293 21
        return $result;
294
    }
295
296 4
    public function tempFilename(string $extension = '', string $location = null): string
297
    {
298 4
        if (empty($location)) {
299 3
            $location = \sys_get_temp_dir();
300
        }
301
302 4
        $filename = \tempnam($location, 'spiral');
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 54
    public function normalizePath(string $path, bool $asDirectory = false): string
315
    {
316 54
        $path = \str_replace(['//', '\\'], '/', $path);
317
318
        //Potentially open links and ../ type directories?
319 54
        return \rtrim($path, '/') . ($asDirectory ? '/' : '');
320
    }
321
322
    /**
323
     * @link http://stackoverflow.com/questions/2637945/getting-relative-path-from-absolute-path-in-php
324
     */
325 16
    public function relativePath(string $path, string $from): string
326
    {
327 16
        $path = $this->normalizePath($path);
328 16
        $from = $this->normalizePath($from);
329
330 16
        $from = \explode('/', $from);
331 16
        $path = \explode('/', $path);
332 16
        $relative = $path;
333
334 16
        foreach ($from as $depth => $dir) {
335
            //Find first non-matching dir
336 16
            if ($dir === $path[$depth]) {
337
                //Ignore this directory
338 16
                \array_shift($relative);
339
            } else {
340
                //Get number of remaining dirs to $from
341 1
                $remaining = \count($from) - $depth;
342 1
                if ($remaining > 1) {
343
                    //Add traversals up to first matching directory
344 1
                    $padLength = (\count($relative) + $remaining - 1) * -1;
345 1
                    $relative = \array_pad($relative, $padLength, '..');
346 1
                    break;
347
                }
348 1
                $relative[0] = './' . $relative[0];
349
            }
350
        }
351
352 16
        return \implode('/', $relative);
353
    }
354
355 21
    private function filesIterator(string $location, string $pattern = null): \GlobIterator
356
    {
357 21
        $pattern ??= '*';
358 21
        $regexp = \rtrim($location, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . \ltrim($pattern, DIRECTORY_SEPARATOR);
359
360 21
        return new \GlobIterator($regexp);
361
    }
362
}
363