Completed
Pull Request — 1.0 (#58)
by Titouan
02:41
created

FilesystemRepository::addResource()   C

Complexity

Conditions 14
Paths 29

Size

Total Lines 71
Code Lines 41

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 42
CRAP Score 14.0185

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 71
ccs 42
cts 44
cp 0.9545
rs 5.5568
cc 14
eloc 41
nc 29
nop 3
crap 14.0185

How to fix   Long Method    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
/*
4
 * This file is part of the puli/repository package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Repository;
13
14
use Iterator;
15
use Puli\Repository\Api\EditableRepository;
16
use Puli\Repository\Api\Resource\BodyResource;
17
use Puli\Repository\Api\Resource\FilesystemResource;
18
use Puli\Repository\Api\Resource\PuliResource;
19
use Puli\Repository\Api\ResourceCollection;
20
use Puli\Repository\Api\ResourceNotFoundException;
21
use Puli\Repository\Api\UnsupportedOperationException;
22
use Puli\Repository\Api\UnsupportedResourceException;
23
use Puli\Repository\ChangeStream\ChangeStream;
24
use Puli\Repository\Resource\Collection\FilesystemResourceCollection;
25
use Puli\Repository\Resource\DirectoryResource;
26
use Puli\Repository\Resource\FileResource;
27
use Puli\Repository\Resource\LinkResource;
28
use RecursiveIteratorIterator;
29
use Symfony\Component\Filesystem\Exception\IOException;
30
use Symfony\Component\Filesystem\Filesystem;
31
use Webmozart\Assert\Assert;
32
use Webmozart\Glob\Iterator\GlobIterator;
33
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
34
use Webmozart\PathUtil\Path;
35
36
/**
37
 * A repository reading from the file system.
38
 *
39
 * Resources can be read using their absolute file system paths:
40
 *
41
 * ```php
42
 * use Puli\Repository\FilesystemRepository;
43
 *
44
 * $repo = new FilesystemRepository();
45
 * $resource = $repo->get('/home/puli/.gitconfig');
46
 * ```
47
 *
48
 * The returned resources implement {@link FilesystemResource}.
49
 *
50
 * Optionally, a root directory can be passed to the constructor. Then all paths
51
 * will be read relative to that directory:
52
 *
53
 * ```php
54
 * $repo = new FilesystemRepository('/home/puli');
55
 * $resource = $repo->get('/.gitconfig');
56
 * ```
57
 *
58
 * While "." and ".." segments are supported, files outside the root directory
59
 * cannot be read. Any leading ".." segments will simply be stripped off.
60
 *
61
 * @since  1.0
62
 *
63
 * @author Bernhard Schussek <[email protected]>
64
 */
65
class FilesystemRepository extends AbstractEditableRepository
66
{
67
    /**
68
     * @var bool|null
69
     */
70
    private static $symlinkSupported;
71
72
    /**
73
     * @var string
74
     */
75
    private $baseDir;
76
77
    /**
78
     * @var bool
79
     */
80
    private $symlink;
81
82
    /**
83
     * @var bool
84
     */
85
    private $relative;
86
87
    /**
88
     * @var Filesystem
89
     */
90
    private $filesystem;
91
92
    /**
93
     * Returns whether symlinks are supported in the local environment.
94
     *
95
     * @return bool Returns `true` if symlinks are supported.
96
     */
97 258
    public static function isSymlinkSupported()
98
    {
99 258
        if (null === self::$symlinkSupported) {
100
            // http://php.net/manual/en/function.symlink.php
101
            // Symlinks are only supported on Windows Vista, Server 2008 or
102
            // greater on PHP 5.3+
103 1
            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
104
                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;
105
            } else {
106 1
                self::$symlinkSupported = true;
107
            }
108 1
        }
109
110 258
        return self::$symlinkSupported;
111
    }
112
113
    /**
114
     * Creates a new repository.
115
     *
116
     * @param string $baseDir  The base directory of the repository on the file
117
     *                         system.
118
     * @param bool   $symlink  Whether to use symbolic links for added files. If
119
     *                         symbolic links are not supported on the current
120
     *                         system, the repository will create hard copies
121
     *                         instead.
122
     * @param bool   $relative Whether to create relative symbolic links. If
123
     *                         relative links are not supported on the current
124
     *                         system, the repository will create absolute links
125
     *                         instead.
126
     * @param ChangeStream|null $changeStream If provided, the repository will log
127
     *                                        resources changes in this change stream.
128
     */
129 332
    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)
130
    {
131 332
        parent::__construct($changeStream);
132
133 332
        Assert::directory($baseDir);
134 332
        Assert::boolean($symlink);
135
136 332
        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');
137 332
        $this->symlink = $symlink && self::isSymlinkSupported();
138 332
        $this->relative = $this->symlink && $relative;
139 332
        $this->filesystem = new Filesystem();
140 332
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 135 View Code Duplication
    public function get($path)
146
    {
147 135
        $path = $this->sanitizePath($path);
148 123
        $filesystemPath = $this->baseDir.$path;
149
150 123
        if (!file_exists($filesystemPath)) {
151 4
            throw ResourceNotFoundException::forPath($path);
152
        }
153
154 119
        return $this->createResource($filesystemPath, $path);
155
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 41
    public function find($query, $language = 'glob')
161
    {
162 41
        return $this->iteratorToCollection($this->getGlobIterator($query, $language));
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168 61
    public function contains($query, $language = 'glob')
169
    {
170 61
        $iterator = $this->getGlobIterator($query, $language);
171 48
        $iterator->rewind();
172
173 48
        return $iterator->valid();
174
    }
175
176
    /**
177
     * {@inheritdoc}
178
     */
179 28
    public function hasChildren($path)
180
    {
181 28
        $filesystemPath = $this->getFilesystemPath($path);
182
183 12
        if (!is_dir($filesystemPath)) {
184 4
            return false;
185
        }
186
187 12
        $iterator = $this->getDirectoryIterator($filesystemPath);
188 12
        $iterator->rewind();
189
190 12
        return $iterator->valid();
191
    }
192
193
    /**
194
     * {@inheritdoc}
195
     */
196 42
    public function listChildren($path)
197
    {
198 42
        $filesystemPath = $this->getFilesystemPath($path);
199
200 26
        if (!is_dir($filesystemPath)) {
201 4
            return new FilesystemResourceCollection();
202
        }
203
204 22
        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210 297
    public function add($path, $resource)
211
    {
212 297
        $path = $this->sanitizePath($path);
213
214 285 View Code Duplication
        if ($resource instanceof ResourceCollection) {
215 4
            $this->ensureDirectoryExists($path);
216 4
            foreach ($resource as $child) {
217 4
                $this->addResource($path.'/'.$child->getName(), $child);
218 4
            }
219
220 4
            return;
221
        }
222
223 281
        if ($resource instanceof PuliResource) {
224 277
            $this->ensureDirectoryExists(Path::getDirectory($path));
225 277
            $this->addResource($path, $resource);
226
227 272
            return;
228
        }
229
230 4
        throw new UnsupportedResourceException(sprintf(
231 4
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
232 4
            is_object($resource) ? get_class($resource) : gettype($resource)
233 4
        ));
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 49
    public function remove($query, $language = 'glob')
240
    {
241 49
        $iterator = $this->getGlobIterator($query, $language);
242 36
        $removed = 0;
243
244 36
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
245
246
        // There's some problem with concurrent deletions at the moment
247 28
        foreach (iterator_to_array($iterator) as $filesystemPath) {
248 28
            $this->removeResource($filesystemPath, $removed);
249 28
        }
250
251 28
        return $removed;
252
    }
253
254
    /**
255
     * {@inheritdoc}
256
     */
257 4
    public function clear()
258
    {
259 4
        $iterator = $this->getDirectoryIterator($this->baseDir);
260 4
        $removed = 0;
261
262 4
        foreach ($iterator as $filesystemPath) {
263 4
            $this->removeResource($filesystemPath, $removed);
264 4
        }
265
266 4
        return $removed;
267
    }
268
269 281
    private function ensureDirectoryExists($path)
270
    {
271 281
        $filesystemPath = $this->baseDir.$path;
272
273 281
        if (is_file($filesystemPath)) {
274 1
            throw new UnsupportedOperationException(sprintf(
275
                'Instances of BodyResource do not support child resources in '.
276 1
                'FilesystemRepository. Tried to add a child to %s.',
277
                $filesystemPath
278 1
            ));
279
        }
280
281 281
        if (!is_dir($filesystemPath)) {
282 80
            mkdir($filesystemPath, 0777, true);
283 80
        }
284 281
    }
285
286 281
    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
287
    {
288 281
        $pathInBaseDir = $this->baseDir.$path;
289 281
        $hasChildren = $resource->hasChildren();
290 281
        $hasBody = $resource instanceof BodyResource;
291
292 281
        if ($hasChildren && $hasBody) {
293 1
            throw new UnsupportedResourceException(sprintf(
294
                'Instances of BodyResource do not support child resources in '.
295 1
                'FilesystemRepository. Tried to add a BodyResource with '.
296 1
                'children at %s.',
297
                $path
298 1
            ));
299
        }
300
301 280
        if ($this->symlink && $checkParentsForSymlinks) {
302 152
            $this->replaceParentSymlinksByCopies($path);
303 152
        }
304
305 280
        if ($resource instanceof FilesystemResource) {
306 37
            if ($this->symlink) {
307 32
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
308 37
            } elseif ($hasBody) {
309 4
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
310 4
            } else {
311 1
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
312
            }
313
314 37
            $this->logChange($path, $resource);
315
316 37
            return;
317
        }
318
319 251
        if ($resource instanceof LinkResource) {
320 8
            if (!$this->symlink) {
321 4
                throw new UnsupportedResourceException(sprintf(
322
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
323 4
                    'Tried to add a LinkResource at %s.',
324
                    $path
325 4
                ));
326
            }
327
328 4
            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);
329
330 4
            $this->logChange($path, $resource);
331
332 4
            return;
333
        }
334
335 243
        if ($hasBody) {
336 119
            file_put_contents($pathInBaseDir, $resource->getBody());
337
338 119
            $this->logChange($path, $resource);
339
340 119
            return;
341
        }
342
343 191
        if (is_file($pathInBaseDir)) {
344
            $this->filesystem->remove($pathInBaseDir);
345
        }
346
347 191
        if (!file_exists($pathInBaseDir)) {
348 115
            mkdir($pathInBaseDir, 0777, true);
349 115
        }
350
351 191
        foreach ($resource->listChildren() as $child) {
352 112
            $this->addResource($path.'/'.$child->getName(), $child, false);
353 191
        }
354
355 191
        $this->logChange($path, $resource);
356 191
    }
357
358 32
    private function removeResource($filesystemPath, &$removed)
359
    {
360
        // Skip paths that have already been removed
361 32
        if (!file_exists($filesystemPath)) {
362
            return;
363
        }
364
365 32
        ++$removed;
366
367 32
        if (is_dir($filesystemPath)) {
368 24
            $removed += $this->countChildren($filesystemPath);
369 24
        }
370
371 32
        $this->filesystem->remove($filesystemPath);
372 32
    }
373
374 119
    private function createResource($filesystemPath, $path)
375
    {
376 119
        $resource = null;
377
378 119
        if (is_link($filesystemPath)) {
379 6
            $baseDir = rtrim($this->baseDir, '/');
380 6
            $targetFilesystemPath = $this->readLink($filesystemPath);
381
382 6
            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
383 6
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
384 6
                $resource = new LinkResource($targetPath);
385 6
            }
386 6
        }
387
388 119
        if (!$resource && is_dir($filesystemPath)) {
389 74
            $resource = new DirectoryResource($filesystemPath);
390 74
        }
391
392 119
        if (!$resource) {
393 67
            $resource = new FileResource($filesystemPath);
394 67
        }
395
396 119
        $resource->attachTo($this, $path);
397
398 119
        return $resource;
399
    }
400
401 25
    private function countChildren($filesystemPath)
402
    {
403 24
        $iterator = new RecursiveIteratorIterator(
404 24
            $this->getDirectoryIterator($filesystemPath),
405
            RecursiveIteratorIterator::SELF_FIRST
406 24
        );
407
408 24
        $iterator->rewind();
409 24
        $count = 0;
410
411 24
        while ($iterator->valid()) {
412 25
            ++$count;
413 24
            $iterator->next();
414 24
        }
415
416 24
        return $count;
417
    }
418
419 50
    private function iteratorToCollection(Iterator $iterator)
420
    {
421 50
        $offset = strlen($this->baseDir);
422 50
        $filesystemPaths = iterator_to_array($iterator);
423 50
        $resources = array();
424
425
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
426 50
        sort($filesystemPaths);
427
428 50
        foreach ($filesystemPaths as $filesystemPath) {
429 42
            $path = substr($filesystemPath, $offset);
430
431 42
            $resource = is_dir($filesystemPath)
432 42
                ? new DirectoryResource($filesystemPath, $path)
433 42
                : new FileResource($filesystemPath, $path);
434
435 42
            $resource->attachTo($this);
436
437 42
            $resources[] = $resource;
438 50
        }
439
440 50
        return new FilesystemResourceCollection($resources);
441
    }
442
443 66 View Code Duplication
    private function getFilesystemPath($path)
444
    {
445 66
        $path = $this->sanitizePath($path);
446 42
        $filesystemPath = $this->baseDir.$path;
447
448 42
        if (!file_exists($filesystemPath)) {
449 8
            throw ResourceNotFoundException::forPath($path);
450
        }
451
452 34
        return $filesystemPath;
453
    }
454
455 123
    private function getGlobIterator($query, $language)
456
    {
457 123
        $this->validateSearchLanguage($language);
458
459 120
        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
460 96
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');
461
462 84
        $query = Path::canonicalize($query);
463
464 84
        return new GlobIterator($this->baseDir.$query);
465
    }
466
467 70
    private function getDirectoryIterator($filesystemPath)
468
    {
469 70
        return new RecursiveDirectoryIterator(
470 70
            $filesystemPath,
471 70
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
472 70
        );
473
    }
474
475 32
    private function symlinkMirror($origin, $target, array $dirsToKeep = array())
476
    {
477 32
        $targetIsDir = is_dir($target);
478 32
        $forceDir = in_array($target, $dirsToKeep, true);
479
480
        // Merge directories
481 32
        if (is_dir($origin) && ($targetIsDir || $forceDir)) {
482 16
            if (is_link($target)) {
483 4
                $this->replaceLinkByCopy($target, $dirsToKeep);
484 4
            }
485
486 16
            $iterator = $this->getDirectoryIterator($origin);
487
488 16
            foreach ($iterator as $path) {
489 16
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
490 16
            }
491
492 16
            return;
493
        }
494
495
        // Replace target
496 32
        if (file_exists($target)) {
497 12
            $this->filesystem->remove($target);
498 12
        }
499
500
        // Try creating a relative link
501 32
        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {
502 16
            return;
503
        }
504
505
        // Try creating a absolute link
506 16
        if ($this->trySymlink($origin, $target)) {
507 16
            return;
508
        }
509
510
        // Fall back to copy
511
        if (is_dir($origin)) {
512
            $this->filesystem->mirror($origin, $target);
513
514
            return;
515
        }
516
517
        $this->filesystem->copy($origin, $target);
518
    }
519
520 152
    private function replaceParentSymlinksByCopies($path)
521
    {
522 152
        $previousPath = null;
523
524
        // Collect all paths that MUST NOT be symlinks after doing the
525
        // replace operation.
526
        //
527
        // Example:
528
        //
529
        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']
530
        //
531
        // Before:
532
        //   /webmozart -> target
533
        //
534
        // After:
535
        //   /webmozart
536
        //     /config -> target/config
537
        //     /views
538
        //       /index.html.twig -> target/views/index.html.twig
539
540 152
        $dirsToKeep = array();
541
542 152
        while ($previousPath !== ($path = Path::getDirectory($path))) {
543 152
            $filesystemPath = $this->baseDir.$path;
544 152
            $dirsToKeep[] = $filesystemPath;
545
546 152
            if (is_link($filesystemPath)) {
547 12
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);
548
549 12
                return;
550
            }
551
552 152
            $previousPath = $path;
553 152
        }
554 152
    }
555
556 16
    private function replaceLinkByCopy($path, array $dirsToKeep = array())
557
    {
558 16
        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));
559 16
        $this->filesystem->remove($path);
560 16
        $this->filesystem->mkdir($path);
561 16
        $this->symlinkMirror($target, $path, $dirsToKeep);
562 16
    }
563
564 32
    private function trySymlink($origin, $target)
565
    {
566
        try {
567 32
            $this->filesystem->symlink($origin, $target);
568
569 32
            if (file_exists($target)) {
570 32
                return true;
571
            }
572
        } catch (IOException $e) {
573
        }
574
575
        return false;
576
    }
577
578 22
    private function readLink($filesystemPath)
579
    {
580
        // On Windows, transitive links are resolved to the final target by
581
        // readlink(). realpath(), however, returns the target link on Windows,
582
        // but not on Unix.
583
584
        // /link1 -> /link2 -> /file
585
586
        // Windows: readlink(/link1) => /file
587
        //          realpath(/link1) => /link2
588
589
        // Unix:    readlink(/link1) => /link2
590
        //          realpath(/link1) => /file
591
592
        // Consistency FTW!
593
594 22
        return '\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);
595
    }
596
}
597