Failed Conditions
Push — 1.0 ( 9f5a0b...fe7a2f )
by Bernhard
30:36 queued 17:00
created

FilesystemRepository::removeResource()   B

Complexity

Conditions 4
Paths 3

Size

Total Lines 22
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 22
ccs 10
cts 10
cp 1
rs 8.9197
cc 4
eloc 10
nc 3
nop 2
crap 4
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\ChangeStream\ChangeStream;
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\Resource\Collection\FilesystemResourceCollection;
24
use Puli\Repository\Resource\DirectoryResource;
25
use Puli\Repository\Resource\FileResource;
26
use Puli\Repository\Resource\LinkResource;
27
use Symfony\Component\Filesystem\Exception\IOException;
28
use Symfony\Component\Filesystem\Filesystem;
29
use Webmozart\Assert\Assert;
30
use Webmozart\Glob\Iterator\GlobIterator;
31
use Webmozart\Glob\Iterator\RecursiveDirectoryIterator;
32
use Webmozart\PathUtil\Path;
33
34
/**
35
 * A repository reading from the file system.
36
 *
37
 * Resources can be read using their absolute file system paths:
38
 *
39
 * ```php
40
 * use Puli\Repository\FilesystemRepository;
41
 *
42
 * $repo = new FilesystemRepository();
43
 * $resource = $repo->get('/home/puli/.gitconfig');
44
 * ```
45
 *
46
 * The returned resources implement {@link FilesystemResource}.
47
 *
48
 * Optionally, a root directory can be passed to the constructor. Then all paths
49
 * will be read relative to that directory:
50
 *
51
 * ```php
52
 * $repo = new FilesystemRepository('/home/puli');
53
 * $resource = $repo->get('/.gitconfig');
54
 * ```
55
 *
56
 * While "." and ".." segments are supported, files outside the root directory
57
 * cannot be read. Any leading ".." segments will simply be stripped off.
58
 *
59
 * @since  1.0
60
 *
61
 * @author Bernhard Schussek <[email protected]>
62
 */
63
class FilesystemRepository extends AbstractEditableRepository
64
{
65
    /**
66
     * @var bool|null
67
     */
68
    private static $symlinkSupported;
69
70
    /**
71
     * @var string
72
     */
73
    private $baseDir;
74
75
    /**
76
     * @var int
77
     */
78
    private $baseDirLength;
79
80
    /**
81
     * @var bool
82
     */
83
    private $symlink;
84
85
    /**
86
     * @var bool
87
     */
88
    private $relative;
89
90
    /**
91
     * @var Filesystem
92
     */
93
    private $filesystem;
94
95
    /**
96
     * Returns whether symlinks are supported in the local environment.
97
     *
98
     * @return bool Returns `true` if symlinks are supported.
99
     */
100 303
    public static function isSymlinkSupported()
101
    {
102 303
        if (null === self::$symlinkSupported) {
103
            // http://php.net/manual/en/function.symlink.php
104
            // Symlinks are only supported on Windows Vista, Server 2008 or
105
            // greater on PHP 5.3+
106 1
            if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
107
                self::$symlinkSupported = PHP_WINDOWS_VERSION_MAJOR >= 6;
108
            } else {
109 1
                self::$symlinkSupported = true;
110
            }
111
        }
112
113 303
        return self::$symlinkSupported;
114
    }
115
116
    /**
117
     * Creates a new repository.
118
     *
119
     * @param string            $baseDir      The base directory of the repository on the file
120
     *                                        system.
121
     * @param bool              $symlink      Whether to use symbolic links for added files. If
122
     *                                        symbolic links are not supported on the current
123
     *                                        system, the repository will create hard copies
124
     *                                        instead.
125
     * @param bool              $relative     Whether to create relative symbolic links. If
126
     *                                        relative links are not supported on the current
127
     *                                        system, the repository will create absolute links
128
     *                                        instead.
129
     * @param ChangeStream|null $changeStream If provided, the repository will log
130
     *                                        resources changes in this change stream.
131
     */
132 392
    public function __construct($baseDir = '/', $symlink = true, $relative = true, ChangeStream $changeStream = null)
133
    {
134 392
        parent::__construct($changeStream);
135
136 392
        Assert::directory($baseDir);
137 392
        Assert::boolean($symlink);
138
139 392
        $this->baseDir = rtrim(Path::canonicalize($baseDir), '/');
140 392
        $this->baseDirLength = strlen($baseDir);
141 392
        $this->symlink = $symlink && self::isSymlinkSupported();
142 392
        $this->relative = $this->symlink && $relative;
143 392
        $this->filesystem = new Filesystem();
144 392
    }
145
146
    /**
147
     * {@inheritdoc}
148
     */
149 159 View Code Duplication
    public function get($path)
150
    {
151 159
        $path = $this->sanitizePath($path);
152 147
        $filesystemPath = $this->baseDir.$path;
153
154 147
        if (!file_exists($filesystemPath)) {
155 8
            throw ResourceNotFoundException::forPath($path);
156
        }
157
158 139
        return $this->createResource($filesystemPath, $path);
159
    }
160
161
    /**
162
     * {@inheritdoc}
163
     */
164 41
    public function find($query, $language = 'glob')
165
    {
166 41
        return $this->iteratorToCollection($this->getGlobIterator($query, $language));
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     */
172 61
    public function contains($query, $language = 'glob')
173
    {
174 61
        $iterator = $this->getGlobIterator($query, $language);
175 48
        $iterator->rewind();
176
177 48
        return $iterator->valid();
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 28
    public function hasChildren($path)
184
    {
185 28
        $filesystemPath = $this->getFilesystemPath($path);
186
187 12
        if (!is_dir($filesystemPath)) {
188 4
            return false;
189
        }
190
191 12
        $iterator = $this->getDirectoryIterator($filesystemPath);
192 12
        $iterator->rewind();
193
194 12
        return $iterator->valid();
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200 42
    public function listChildren($path)
201
    {
202 42
        $filesystemPath = $this->getFilesystemPath($path);
203
204 26
        if (!is_dir($filesystemPath)) {
205 4
            return new FilesystemResourceCollection();
206
        }
207
208 22
        return $this->iteratorToCollection($this->getDirectoryIterator($filesystemPath));
209
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 357
    public function add($path, $resource)
215
    {
216 357
        $path = $this->sanitizePath($path);
217
218 345 View Code Duplication
        if ($resource instanceof ResourceCollection) {
219 4
            $this->ensureDirectoryExists($path);
220 4
            foreach ($resource as $child) {
221 4
                $this->addResource($path.'/'.$child->getName(), $child);
222
            }
223
224 4
            return;
225
        }
226
227 341
        if ($resource instanceof PuliResource) {
228 337
            $this->ensureDirectoryExists(Path::getDirectory($path));
229 337
            $this->addResource($path, $resource);
230
231 332
            return;
232
        }
233
234 4
        throw new UnsupportedResourceException(sprintf(
235 4
            'The passed resource must be a PuliResource or ResourceCollection. Got: %s',
236 4
            is_object($resource) ? get_class($resource) : gettype($resource)
237
        ));
238
    }
239
240
    /**
241
     * {@inheritdoc}
242
     */
243 61
    public function remove($query, $language = 'glob')
244
    {
245 61
        $iterator = $this->getGlobIterator($query, $language);
246 48
        $removed = 0;
247
248 48
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
249
250
        // There's some problem with concurrent deletions at the moment
251 40
        foreach (iterator_to_array($iterator) as $filesystemPath) {
252 40
            $this->removeResource($filesystemPath, $removed);
253
        }
254
255 40
        return $removed;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261 12
    public function clear()
262
    {
263 12
        $iterator = $this->getDirectoryIterator($this->baseDir);
264 12
        $removed = 0;
265
266
        // Batch-delete all versions
267 12
        $this->clearVersions();
268
269 12
        foreach ($iterator as $filesystemPath) {
270 8
            $this->removeResource($filesystemPath, $removed);
271
        }
272
273 12
        $this->storeVersion($this->get('/'));
274
275 12
        return $removed;
276
    }
277
278 341
    private function ensureDirectoryExists($path)
279
    {
280 341
        $filesystemPath = $this->baseDir.$path;
281
282 341
        if (is_file($filesystemPath)) {
283 1
            throw new UnsupportedOperationException(sprintf(
284
                'Instances of BodyResource do not support child resources in '.
285 1
                'FilesystemRepository. Tried to add a child to %s.',
286
                $filesystemPath
287
            ));
288
        }
289
290 341
        if (!is_dir($filesystemPath)) {
291 120
            mkdir($filesystemPath, 0777, true);
292
        }
293 341
    }
294
295 341
    private function addResource($path, PuliResource $resource, $checkParentsForSymlinks = true)
296
    {
297 341
        $pathInBaseDir = $this->baseDir.$path;
298 341
        $hasChildren = $resource->hasChildren();
299 341
        $hasBody = $resource instanceof BodyResource;
300
301 341
        if ($hasChildren && $hasBody) {
302 1
            throw new UnsupportedResourceException(sprintf(
303
                'Instances of BodyResource do not support child resources in '.
304
                'FilesystemRepository. Tried to add a BodyResource with '.
305 1
                'children at %s.',
306
                $path
307
            ));
308
        }
309
310
        // Don't modify resources attached to other repositories
311 340
        if ($resource->isAttached()) {
312 4
            $resource = clone $resource;
313
        }
314
315 340
        $resource->attachTo($this, $path);
316
317 340
        if ($this->symlink && $checkParentsForSymlinks) {
318 182
            $this->replaceParentSymlinksByCopies($path);
319
        }
320
321 340
        if ($resource instanceof FilesystemResource) {
322 33
            if ($this->symlink) {
323 30
                $this->symlinkMirror($resource->getFilesystemPath(), $pathInBaseDir);
324 3
            } elseif ($hasBody) {
325 2
                $this->filesystem->copy($resource->getFilesystemPath(), $pathInBaseDir);
326
            } else {
327 1
                $this->filesystem->mirror($resource->getFilesystemPath(), $pathInBaseDir);
328
            }
329
330 33
            $this->storeVersion($resource);
331
332 33
            return;
333
        }
334
335 315
        if ($resource instanceof LinkResource) {
336 8
            if (!$this->symlink) {
337 4
                throw new UnsupportedResourceException(sprintf(
338
                    'LinkResource requires support of symbolic links in FilesystemRepository. '.
339 4
                    'Tried to add a LinkResource at %s.',
340
                    $path
341
                ));
342
            }
343
344 4
            $this->filesystem->symlink($this->baseDir.$resource->getTargetPath(), $pathInBaseDir);
345
346 4
            $this->storeVersion($resource);
347
348 4
            return;
349
        }
350
351 307
        if ($hasBody) {
352 175
            file_put_contents($pathInBaseDir, $resource->getBody());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Puli\Repository\Api\Resource\PuliResource as the method getBody() does only exist in the following implementations of said interface: Puli\Repository\Resource\FileResource.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
353
354 175
            $this->storeVersion($resource);
355
356 175
            return;
357
        }
358
359 231
        if (is_file($pathInBaseDir)) {
360
            $this->filesystem->remove($pathInBaseDir);
361
        }
362
363 231
        if (!file_exists($pathInBaseDir)) {
364 139
            mkdir($pathInBaseDir, 0777, true);
365
        }
366
367 231
        foreach ($resource->listChildren() as $child) {
368 140
            $this->addResource($path.'/'.$child->getName(), $child, false);
369
        }
370
371 231
        $this->storeVersion($resource);
372 231
    }
373
374 48
    private function removeResource($filesystemPath, &$removed)
375
    {
376
        // Skip paths that have already been removed
377 48
        if (!file_exists($filesystemPath)) {
378
            return;
379
        }
380
381 48
        $this->removeVersions($this->getPath($filesystemPath));
382
383 48
        ++$removed;
384
385 48
        if (is_dir($filesystemPath)) {
386 32
            $iterator = $this->getDirectoryIterator($filesystemPath);
387
388 32
            foreach ($iterator as $childFilesystemPath) {
389
                // Remove children and child versions
390 32
                $this->removeResource($childFilesystemPath, $removed);
391
            }
392
        }
393
394 48
        $this->filesystem->remove($filesystemPath);
395 48
    }
396
397 139
    private function createResource($filesystemPath, $path)
398
    {
399 139
        $resource = null;
400
401 139
        if (is_link($filesystemPath)) {
402 6
            $baseDir = rtrim($this->baseDir, '/');
403 6
            $targetFilesystemPath = $this->readLink($filesystemPath);
404
405 6
            if (Path::isBasePath($baseDir, $targetFilesystemPath)) {
406 6
                $targetPath = '/'.Path::makeRelative($targetFilesystemPath, $baseDir);
407 6
                $resource = new LinkResource($targetPath);
408
            }
409
        }
410
411 139
        if (!$resource && is_dir($filesystemPath)) {
412 90
            $resource = new DirectoryResource($filesystemPath);
413
        }
414
415 139
        if (!$resource) {
416 71
            $resource = new FileResource($filesystemPath);
417
        }
418
419 139
        $resource->attachTo($this, $path);
420
421 139
        return $resource;
422
    }
423
424 50
    private function iteratorToCollection(Iterator $iterator)
425
    {
426 50
        $filesystemPaths = iterator_to_array($iterator);
427 50
        $resources = array();
428
429
        // RecursiveDirectoryIterator is not guaranteed to return sorted results
430 50
        sort($filesystemPaths);
431
432 50
        foreach ($filesystemPaths as $filesystemPath) {
433 42
            $resource = is_dir($filesystemPath)
434 24
                ? new DirectoryResource($filesystemPath, $this->getPath($filesystemPath))
435 42
                : new FileResource($filesystemPath, $this->getPath($filesystemPath));
436
437 42
            $resource->attachTo($this);
438
439 42
            $resources[] = $resource;
440
        }
441
442 50
        return new FilesystemResourceCollection($resources);
443
    }
444
445 66 View Code Duplication
    private function getFilesystemPath($path)
446
    {
447 66
        $path = $this->sanitizePath($path);
448 42
        $filesystemPath = $this->baseDir.$path;
449
450 42
        if (!file_exists($filesystemPath)) {
451 8
            throw ResourceNotFoundException::forPath($path);
452
        }
453
454 34
        return $filesystemPath;
455
    }
456
457 135
    private function getGlobIterator($query, $language)
458
    {
459 135
        $this->failUnlessGlob($language);
460
461 132
        Assert::stringNotEmpty($query, 'The glob must be a non-empty string. Got: %s');
462 108
        Assert::startsWith($query, '/', 'The glob %s is not absolute.');
463
464 96
        $query = Path::canonicalize($query);
465
466 96
        return new GlobIterator($this->baseDir.$query);
467
    }
468
469 82
    private function getDirectoryIterator($filesystemPath)
470
    {
471 82
        return new RecursiveDirectoryIterator(
472
            $filesystemPath,
473 82
            RecursiveDirectoryIterator::CURRENT_AS_PATHNAME | RecursiveDirectoryIterator::SKIP_DOTS
474
        );
475
    }
476
477 30
    private function symlinkMirror($origin, $target, array $dirsToKeep = array())
478
    {
479 30
        $targetIsDir = is_dir($target);
480 30
        $forceDir = in_array($target, $dirsToKeep, true);
481
482
        // Merge directories
483 30
        if (is_dir($origin) && ($targetIsDir || $forceDir)) {
484 16
            if (is_link($target)) {
485 4
                $this->replaceLinkByCopy($target, $dirsToKeep);
486
            }
487
488 16
            $iterator = $this->getDirectoryIterator($origin);
489
490 16
            foreach ($iterator as $path) {
491 16
                $this->symlinkMirror($path, $target.'/'.basename($path), $dirsToKeep);
492
            }
493
494 16
            return;
495
        }
496
497
        // Replace target
498 30
        if (file_exists($target)) {
499 10
            $this->filesystem->remove($target);
500
        }
501
502
        // Try creating a relative link
503 30
        if ($this->relative && $this->trySymlink(Path::makeRelative($origin, Path::getDirectory($target)), $target)) {
504 15
            return;
505
        }
506
507
        // Try creating a absolute link
508 15
        if ($this->trySymlink($origin, $target)) {
509 15
            return;
510
        }
511
512
        // Fall back to copy
513
        if (is_dir($origin)) {
514
            $this->filesystem->mirror($origin, $target);
515
516
            return;
517
        }
518
519
        $this->filesystem->copy($origin, $target);
520
    }
521
522 182
    private function replaceParentSymlinksByCopies($path)
523
    {
524 182
        $previousPath = null;
525
526
        // Collect all paths that MUST NOT be symlinks after doing the
527
        // replace operation.
528
        //
529
        // Example:
530
        //
531
        // $dirsToKeep = ['/path/to/webmozart', '/path/to/webmozart/views']
532
        //
533
        // Before:
534
        //   /webmozart -> target
535
        //
536
        // After:
537
        //   /webmozart
538
        //     /config -> target/config
539
        //     /views
540
        //       /index.html.twig -> target/views/index.html.twig
541
542 182
        $dirsToKeep = array();
543
544 182
        while ($previousPath !== ($path = Path::getDirectory($path))) {
545 182
            $filesystemPath = $this->baseDir.$path;
546 182
            $dirsToKeep[] = $filesystemPath;
547
548 182
            if (is_link($filesystemPath)) {
549 12
                $this->replaceLinkByCopy($filesystemPath, $dirsToKeep);
550
551 12
                return;
552
            }
553
554 182
            $previousPath = $path;
555
        }
556 182
    }
557
558 16
    private function replaceLinkByCopy($path, array $dirsToKeep = array())
559
    {
560 16
        $target = Path::makeAbsolute($this->readLink($path), Path::getDirectory($path));
561 16
        $this->filesystem->remove($path);
562 16
        $this->filesystem->mkdir($path);
563 16
        $this->symlinkMirror($target, $path, $dirsToKeep);
564 16
    }
565
566 30
    private function trySymlink($origin, $target)
567
    {
568
        try {
569 30
            $this->filesystem->symlink($origin, $target);
570
571 30
            if (file_exists($target)) {
572 30
                return true;
573
            }
574
        } catch (IOException $e) {
575
        }
576
577
        return false;
578
    }
579
580 22
    private function readLink($filesystemPath)
581
    {
582
        // On Windows, transitive links are resolved to the final target by
583
        // readlink(). realpath(), however, returns the target link on Windows,
584
        // but not on Unix.
585
586
        // /link1 -> /link2 -> /file
587
588
        // Windows: readlink(/link1) => /file
589
        //          realpath(/link1) => /link2
590
591
        // Unix:    readlink(/link1) => /link2
592
        //          realpath(/link1) => /file
593
594
        // Consistency FTW!
595
596 22
        return '\\' === DIRECTORY_SEPARATOR ? realpath($filesystemPath) : readlink($filesystemPath);
597
    }
598
599 90
    private function getPath($filesystemPath)
600
    {
601 90
        return substr($filesystemPath, $this->baseDirLength);
602
    }
603
}
604