Passed
Push — master ( c6a6de...ed87d4 )
by Kirill
02:46
created

Files::write()   A

Complexity

Conditions 6
Paths 21

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 15
c 1
b 1
f 0
dl 0
loc 34
rs 9.2222
cc 6
nc 21
nop 5
1
<?php
2
3
/**
4
 * Spiral Framework.
5
 *
6
 * @license   MIT
7
 * @author    Anton Titov (Wolfy-J)
8
 */
9
10
declare(strict_types=1);
11
12
namespace Spiral\Files;
13
14
use Spiral\Files\Exception\FileNotFoundException;
15
use Spiral\Files\Exception\FilesException;
16
use Spiral\Files\Exception\WriteErrorException;
17
18
/**
19
 * Default abstraction for file management operations.
20
 */
21
final class Files implements FilesInterface
22
{
23
    /**
24
     * Default file mode for this manager.
25
     */
26
    public const DEFAULT_FILE_MODE = self::READONLY;
27
28
    /**
29
     * Files to be removed when component destructed.
30
     *
31
     * @var array
32
     */
33
    private $destructFiles = [];
34
35
    /**
36
     * FileManager constructor.
37
     */
38
    public function __construct()
39
    {
40
        register_shutdown_function([$this, '__destruct']);
41
    }
42
43
    /**
44
     * Destruct every temporary file.
45
     */
46
    public function __destruct()
47
    {
48
        foreach ($this->destructFiles as $filename) {
49
            $this->delete($filename);
50
        }
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     *
56
     * @param bool $recursivePermissions Propagate permissions on created directories.
57
     */
58
    public function ensureDirectory(
59
        string $directory,
60
        int $mode = null,
61
        bool $recursivePermissions = true
62
    ): bool {
63
        if (empty($mode)) {
64
            $mode = self::DEFAULT_FILE_MODE;
65
        }
66
67
        //Directories always executable
68
        $mode = $mode | 0111;
69
        if (is_dir($directory)) {
70
            //Exists :(
71
            return $this->setPermissions($directory, $mode);
72
        }
73
74
        if (!$recursivePermissions) {
75
            return mkdir($directory, $mode, true);
76
        }
77
78
        $directoryChain = [basename($directory)];
79
80
        $baseDirectory = $directory;
81
        while (!is_dir($baseDirectory = dirname($baseDirectory))) {
82
            $directoryChain[] = basename($baseDirectory);
83
        }
84
85
        foreach (array_reverse($directoryChain) as $directory) {
86
            if (!mkdir($baseDirectory = "{$baseDirectory}/{$directory}")) {
87
                return false;
88
            }
89
90
            chmod($baseDirectory, $mode);
91
        }
92
93
        return true;
94
    }
95
96
    /**
97
     * {@inheritdoc}
98
     */
99
    public function read(string $filename): string
100
    {
101
        if (!$this->exists($filename)) {
102
            throw new FileNotFoundException($filename);
103
        }
104
105
        return file_get_contents($filename);
106
    }
107
108
    /**
109
     * {@inheritdoc}
110
     *
111
     * @param bool $append To append data at the end of existed file.
112
     */
113
    public function write(
114
        string $filename,
115
        string $data,
116
        int $mode = null,
117
        bool $ensureDirectory = false,
118
        bool $append = false
119
    ): bool {
120
        $mode = $mode ?? self::DEFAULT_FILE_MODE;
121
122
        try {
123
            if ($ensureDirectory) {
124
                $this->ensureDirectory(dirname($filename), $mode);
125
            }
126
127
            if ($this->exists($filename)) {
128
                //Forcing mode for existed file
129
                $this->setPermissions($filename, $mode);
130
            }
131
132
            $result = file_put_contents(
133
                $filename,
134
                $data,
135
                $append ? FILE_APPEND | LOCK_EX : LOCK_EX
136
            );
137
138
            if ($result !== false) {
139
                //Forcing mode after file creation
140
                $this->setPermissions($filename, $mode);
141
            }
142
        } catch (\Exception $e) {
143
            throw new WriteErrorException($e->getMessage(), $e->getCode(), $e);
144
        }
145
146
        return $result !== false;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152
    public function append(
153
        string $filename,
154
        string $data,
155
        int $mode = null,
156
        bool $ensureDirectory = false
157
    ): bool {
158
        return $this->write($filename, $data, $mode, $ensureDirectory, true);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164
    public function delete(string $filename)
165
    {
166
        if ($this->exists($filename)) {
167
            $result = unlink($filename);
168
169
            //Wiping out changes in local file cache
170
            clearstatcache(false, $filename);
171
172
            return $result;
173
        }
174
175
        return false;
176
    }
177
178
    /**
179
     * {@inheritdoc}
180
     *
181
     * @see http://stackoverflow.com/questions/3349753/delete-directory-with-files-in-it
182
     *
183
     * @param string $directory
184
     * @param bool   $contentOnly
185
     *
186
     * @throws FilesException
187
     */
188
    public function deleteDirectory(string $directory, bool $contentOnly = false): void
189
    {
190
        if (!$this->isDirectory($directory)) {
191
            throw new FilesException("Undefined or invalid directory {$directory}");
192
        }
193
194
        $files = new \RecursiveIteratorIterator(
195
            new \RecursiveDirectoryIterator($directory, \RecursiveDirectoryIterator::SKIP_DOTS),
196
            \RecursiveIteratorIterator::CHILD_FIRST
197
        );
198
199
        foreach ($files as $file) {
200
            if ($file->isDir()) {
201
                rmdir($file->getRealPath());
202
            } else {
203
                $this->delete($file->getRealPath());
204
            }
205
        }
206
207
        if (!$contentOnly) {
208
            rmdir($directory);
209
        }
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function move(string $filename, string $destination): bool
216
    {
217
        if (!$this->exists($filename)) {
218
            throw new FileNotFoundException($filename);
219
        }
220
221
        return rename($filename, $destination);
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227
    public function copy(string $filename, string $destination): bool
228
    {
229
        if (!$this->exists($filename)) {
230
            throw new FileNotFoundException($filename);
231
        }
232
233
        return copy($filename, $destination);
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239
    public function touch(string $filename, int $mode = null): bool
240
    {
241
        if (!touch($filename)) {
242
            return false;
243
        }
244
245
        return $this->setPermissions($filename, $mode ?? self::DEFAULT_FILE_MODE);
246
    }
247
248
    /**
249
     * {@inheritdoc}
250
     */
251
    public function exists(string $filename): bool
252
    {
253
        return file_exists($filename);
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259
    public function size(string $filename): int
260
    {
261
        if (!$this->exists($filename)) {
262
            throw new FileNotFoundException($filename);
263
        }
264
265
        return filesize($filename);
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271
    public function extension(string $filename): string
272
    {
273
        return strtolower(pathinfo($filename, PATHINFO_EXTENSION));
274
    }
275
276
    /**
277
     * {@inheritdoc}
278
     */
279
    public function md5(string $filename): string
280
    {
281
        if (!$this->exists($filename)) {
282
            throw new FileNotFoundException($filename);
283
        }
284
285
        return md5_file($filename);
286
    }
287
288
    /**
289
     * {@inheritdoc}
290
     */
291
    public function time(string $filename): int
292
    {
293
        if (!$this->exists($filename)) {
294
            throw new FileNotFoundException($filename);
295
        }
296
297
        return filemtime($filename);
298
    }
299
300
    /**
301
     * {@inheritdoc}
302
     */
303
    public function isDirectory(string $filename): bool
304
    {
305
        return is_dir($filename);
306
    }
307
308
    /**
309
     * {@inheritdoc}
310
     */
311
    public function isFile(string $filename): bool
312
    {
313
        return is_file($filename);
314
    }
315
316
    /**
317
     * {@inheritdoc}
318
     */
319
    public function getPermissions(string $filename): int
320
    {
321
        if (!$this->exists($filename)) {
322
            throw new FileNotFoundException($filename);
323
        }
324
325
        return fileperms($filename) & 0777;
326
    }
327
328
    /**
329
     * {@inheritdoc}
330
     */
331
    public function setPermissions(string $filename, int $mode)
332
    {
333
        if (is_dir($filename)) {
334
            //Directories must always be executable (i.e. 664 for dir => 775)
335
            $mode |= 0111;
336
        }
337
338
        return $this->getPermissions($filename) == $mode || chmod($filename, $mode);
339
    }
340
341
    /**
342
     * {@inheritdoc}
343
     */
344
    public function getFiles(string $location, string $pattern = null): array
345
    {
346
        $result = [];
347
        foreach ($this->filesIterator($location, $pattern) as $filename) {
348
            if ($this->isDirectory($filename->getPathname())) {
349
                $result = array_merge($result, $this->getFiles($filename . DIRECTORY_SEPARATOR));
350
351
                continue;
352
            }
353
354
            $result[] = $this->normalizePath((string)$filename);
355
        }
356
357
        return $result;
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363
    public function tempFilename(string $extension = '', string $location = null): string
364
    {
365
        if (empty($location)) {
366
            $location = sys_get_temp_dir();
367
        }
368
369
        $filename = tempnam($location, 'spiral');
370
371
        if (!empty($extension)) {
372
            //I should find more original way of doing that
373
            rename($filename, $filename = "{$filename}.{$extension}");
374
            $this->destructFiles[] = $filename;
375
        }
376
377
        return $filename;
378
    }
379
380
    /**
381
     * {@inheritdoc}
382
     */
383
    public function normalizePath(string $path, bool $asDirectory = false): string
384
    {
385
        $path = str_replace(['//', '\\'], '/', $path);
386
387
        //Potentially open links and ../ type directories?
388
        return rtrim($path, '/') . ($asDirectory ? '/' : '');
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     *
394
     * @link http://stackoverflow.com/questions/2637945/getting-relative-path-from-absolute-path-in-php
395
     */
396
    public function relativePath(string $path, string $from): string
397
    {
398
        $path = $this->normalizePath($path);
399
        $from = $this->normalizePath($from);
400
401
        $from = explode('/', $from);
402
        $path = explode('/', $path);
403
        $relative = $path;
404
405
        foreach ($from as $depth => $dir) {
406
            //Find first non-matching dir
407
            if ($dir === $path[$depth]) {
408
                //Ignore this directory
409
                array_shift($relative);
410
            } else {
411
                //Get number of remaining dirs to $from
412
                $remaining = count($from) - $depth;
413
                if ($remaining > 1) {
414
                    //Add traversals up to first matching directory
415
                    $padLength = (count($relative) + $remaining - 1) * -1;
416
                    $relative = array_pad($relative, $padLength, '..');
417
                    break;
418
                }
419
                $relative[0] = './' . $relative[0];
420
            }
421
        }
422
423
        return implode('/', $relative);
424
    }
425
426
    /**
427
     * @param string      $location
428
     * @param string|null $pattern
429
     *
430
     * @return \GlobIterator|\SplFileInfo[]
431
     */
432
    private function filesIterator(string $location, string $pattern = null): \GlobIterator
433
    {
434
        $pattern = $pattern ?? '*';
435
        $regexp = rtrim($location, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . ltrim($pattern, DIRECTORY_SEPARATOR);
436
437
        return new \GlobIterator($regexp);
438
    }
439
}
440