Completed
Push — master ( 84196b...176928 )
by Anton
05:27
created

FileManager::relativePath()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 30
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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