Completed
Push — master ( fef7a6...aff413 )
by Arne
02:38
created

Vault   F

Complexity

Total Complexity 75

Size/Duplication

Total Lines 625
Duplicated Lines 7.04 %

Coupling/Cohesion

Components 1
Dependencies 25

Importance

Changes 0
Metric Value
wmc 75
lcom 1
cbo 25
dl 44
loc 625
rs 1.2391
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A getConfiguration() 0 4 1
A getVaultConfiguration() 0 4 1
A getStorageDriver() 0 12 2
A getLockAdapter() 0 13 2
A getIndexMerger() 0 11 2
A getConflictHandler() 0 11 2
A getOperationListBuilder() 0 11 2
A buildLocalIndex() 0 4 1
A loadLastLocalIndex() 0 16 2
A loadRemoteIndex() 11 18 3
A buildMergedIndex() 0 4 1
A getOperationList() 0 10 1
C synchronize() 11 102 8
A loadSynchronizationList() 0 20 2
A restore() 0 4 1
A dump() 0 4 1
B doBuildLocalIndex() 0 32 5
B doLoadRemoteIndex() 0 29 4
B doBuildMergedIndex() 0 13 5
C doRestore() 22 73 10
A readIndexFromStream() 0 16 3
A writeIndexToFile() 0 16 3
A readSynchronizationListFromStream() 0 16 3
A writeSynchronizationListToTemporaryFile() 0 19 3
A generateNewBlobId() 0 10 2
A initMetadataDirectory() 0 14 3
A getLastLocalIndexFilePath() 0 4 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Vault often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Vault, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Archivr;
4
5
use Archivr\ConflictHandler\ConflictHandlerFactory;
6
use Archivr\ConflictHandler\ConflictHandlerInterface;
7
use Archivr\IndexMerger\IndexMergerFactory;
8
use Archivr\LockAdapter\LockAdapterFactory;
9
use Archivr\OperationListBuilder\OperationListBuilderFactory;
10
use Archivr\StorageDriver\StorageDriverFactory;
11
use Archivr\StorageDriver\StorageDriverInterface;
12
use Archivr\Exception\Exception;
13
use Archivr\IndexMerger\IndexMergerInterface;
14
use Archivr\LockAdapter\LockAdapterInterface;
15
use Archivr\OperationListBuilder\OperationListBuilderInterface;
16
use Archivr\SynchronizationProgressListener\DummySynchronizationProgressListener;
17
use Archivr\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
18
use Ramsey\Uuid\Uuid;
19
use Symfony\Component\Finder\Finder;
20
use Symfony\Component\Finder\SplFileInfo;
21
use Archivr\Operation\OperationInterface;
22
23
class Vault
24
{
25
    const METADATA_DIRECTORY_NAME = '.archivr';
26
    const SYNCHRONIZATION_LIST_FILE_NAME = 'index';
27
    const LOCK_SYNC = 'sync';
28
29
    /**
30
     * @var Configuration
31
     */
32
    protected $configuration;
33
34
    /**
35
     * @var VaultConfiguration
36
     */
37
    protected $vaultConfiguration;
38
39
    /**
40
     * @var StorageDriverInterface
41
     */
42
    protected $storageDriver;
43
44
    /**
45
     * @var LockAdapterInterface
46
     */
47
    protected $lockAdapter;
48
49
    /**
50
     * @var IndexMergerInterface
51
     */
52
    protected $indexMerger;
53
54
    /**
55
     * @var ConflictHandlerInterface
56
     */
57
    protected $conflictHandler;
58
59
    /**
60
     * @var OperationListBuilderInterface
61
     */
62
    protected $operationListBuilder;
63
64
    public function __construct(Configuration $configuration, VaultConfiguration $vaultConfiguration)
65
    {
66
        $this->configuration = $configuration;
67
        $this->vaultConfiguration = $vaultConfiguration;
68
    }
69
70
    public function getConfiguration(): Configuration
71
    {
72
        return $this->configuration;
73
    }
74
75
    public function getVaultConfiguration(): VaultConfiguration
76
    {
77
        return $this->vaultConfiguration;
78
    }
79
80
    public function getStorageDriver(): StorageDriverInterface
81
    {
82
        if ($this->storageDriver === null)
83
        {
84
            $this->storageDriver = StorageDriverFactory::create(
85
                $this->vaultConfiguration->getStorageDriver(),
86
                $this->vaultConfiguration
87
            );
88
        }
89
90
        return $this->storageDriver;
91
    }
92
93
    public function getLockAdapter(): LockAdapterInterface
94
    {
95
        if ($this->lockAdapter === null)
96
        {
97
            $this->lockAdapter = LockAdapterFactory::create(
98
                $this->vaultConfiguration->getLockAdapter(),
99
                $this->vaultConfiguration,
100
                $this->getStorageDriver()
101
            );
102
        }
103
104
        return $this->lockAdapter;
105
    }
106
107
    public function getIndexMerger(): IndexMergerInterface
108
    {
109
        if ($this->indexMerger === null)
110
        {
111
            $this->indexMerger = IndexMergerFactory::create(
112
                $this->vaultConfiguration->getIndexMerger()
113
            );
114
        }
115
116
        return $this->indexMerger;
117
    }
118
119
    public function getConflictHandler(): ConflictHandlerInterface
120
    {
121
        if ($this->conflictHandler === null)
122
        {
123
            $this->conflictHandler = ConflictHandlerFactory::create(
124
                $this->vaultConfiguration->getConflictHandler()
125
            );
126
        }
127
128
        return $this->conflictHandler;
129
    }
130
131
    public function getOperationListBuilder(): OperationListBuilderInterface
132
    {
133
        if ($this->operationListBuilder === null)
134
        {
135
            $this->operationListBuilder = OperationListBuilderFactory::create(
136
                $this->vaultConfiguration->getOperationListBuilder()
137
            );
138
        }
139
140
        return $this->operationListBuilder;
141
    }
142
143
    /**
144
     * Builds and returns an index representing the current local state.
145
     *
146
     * @return Index
147
     */
148
    public function buildLocalIndex(): Index
149
    {
150
        return $this->doBuildLocalIndex();
151
    }
152
153
    /**
154
     * Reads and returns the index representing the local state on the last synchronization.
155
     *
156
     * @return Index
157
     * @throws Exception
158
     */
159
    public function loadLastLocalIndex()
160
    {
161
        $index = null;
162
        $path = $this->getLastLocalIndexFilePath();
163
164
        if (is_file($path))
165
        {
166
            $stream = fopen($path, 'rb');
167
168
            $index = $this->readIndexFromStream($stream);
169
170
            fclose($stream);
171
        }
172
173
        return $index;
174
    }
175
176
    /**
177
     * Reads and returns the current remote index.
178
     *
179
     * @param int $revision Revision to load. Defaults to the last revision.
180
     *
181
     * @return Index
182
     */
183
    public function loadRemoteIndex(int $revision = null)
184
    {
185
        $list = null;
186
187 View Code Duplication
        if ($revision === null)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
188
        {
189
            $list = $this->loadSynchronizationList();
190
191
            if (!$list->getLastSynchronization())
192
            {
193
                return null;
194
            }
195
196
            $revision = $list->getLastSynchronization()->getRevision();
197
        }
198
199
        return $this->doLoadRemoteIndex($revision, $list);
200
    }
201
202
    /**
203
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
204
     *
205
     * @return Index
206
     */
207
    public function buildMergedIndex(): Index
208
    {
209
        return $this->doBuildMergedIndex();
210
    }
211
212
    /**
213
     * Returns ordered list of operations required to synchronize the vault with the local path.
214
     * In addition to the object specific operations contained in the returned OperationList additional operations
215
     * might be necessary like index updates that do not belong to specific index objects.
216
     *
217
     * @return OperationList
218
     */
219
    public function getOperationList(): OperationList
220
    {
221
        $localIndex = $this->buildLocalIndex();
222
        $lastLocalIndex = $this->loadLastLocalIndex();
223
        $remoteIndex = $this->loadRemoteIndex();
224
225
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
226
227
        return $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
228
    }
229
230
    /**
231
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
232
     *
233
     * @param int $newRevision
234
     * @param SynchronizationProgressListenerInterface $progressionListener
235
     *
236
     * @return OperationResultList
237
     * @throws Exception
238
     */
239
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
240
    {
241
        if ($progressionListener === null)
242
        {
243
            $progressionListener = new DummySynchronizationProgressListener();
244
        }
245
246
        $localIndex = $this->buildLocalIndex();
247
        $lastLocalIndex = $this->loadLastLocalIndex();
248
249
250
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
251
        {
252
            throw new Exception('Failed to acquire lock.');
253
        }
254
255
256
        $synchronizationList = $this->loadSynchronizationList();
257
        $lastSynchronization = $synchronizationList->getLastSynchronization();
258
259
        if ($lastSynchronization)
260
        {
261
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
262
            $remoteIndex = $this->doLoadRemoteIndex($lastSynchronization->getRevision(), $synchronizationList);
263
        }
264
        else
265
        {
266
            $newRevision = $newRevision ?: 1;
267
            $remoteIndex = null;
268
        }
269
270
        $synchronization = new Synchronization($newRevision, $this->generateNewBlobId(), new \DateTime(), $this->configuration->getIdentity());
271
        $synchronizationList->addSynchronization($synchronization);
272
273
        // compute merged index
274
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
275
276
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
277
278
        $operationResultList = new OperationResultList();
279
280
        // operation count +
281
        // merged index write +
282
        // copy merged index to vault +
283
        // save merged index as last local index +
284
        // upload synchronization list +
285
        // release lock
286
        $progressionListener->start(count($operationList) + 5);
287
288 View Code Duplication
        foreach ($operationList as $operation)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
289
        {
290
            /** @var OperationInterface $operation */
291
292
            $success = $operation->execute($this->configuration->getLocalPath(), $this->getStorageDriver());
293
294
            $operationResult = new OperationResult($operation, $success);
295
            $operationResultList->addOperationResult($operationResult);
296
297
            $progressionListener->advance();
298
        }
299
300
        // dump new index
301
        $mergedIndexFilePath = tempnam(sys_get_temp_dir(), 'index');
302
        $this->writeIndexToFile($mergedIndex, $mergedIndexFilePath);
303
304
        $progressionListener->advance();
305
306
        // upload new index
307
        $readStream = fopen($mergedIndexFilePath, 'rb');
308
        $compressionFilter = stream_filter_append($readStream, 'zlib.deflate');
309
        $this->storageDriver->writeStream($synchronization->getBlobId(), $readStream);
310
        rewind($readStream);
311
        stream_filter_remove($compressionFilter);
312
313
        $progressionListener->advance();
314
315
        // save new index locally
316
        $writeStream = fopen($this->getLastLocalIndexFilePath(), 'wb');
317
        stream_copy_to_stream($readStream, $writeStream);
318
        fclose($writeStream);
319
        fclose($readStream);
320
321
        $progressionListener->advance();
322
323
        // upload new synchronization list
324
        $synchronizationListFilePath = $this->writeSynchronizationListToTemporaryFile($synchronizationList);
325
        $readStream = fopen($synchronizationListFilePath, 'rb');
326
        stream_filter_append($readStream, 'zlib.deflate');
327
        $this->storageDriver->writeStream(static::SYNCHRONIZATION_LIST_FILE_NAME, $readStream);
328
        fclose($readStream);
329
330
        // release lock
331
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
332
        {
333
            throw new Exception('Failed to release lock.');
334
        }
335
336
        $progressionListener->advance();
337
        $progressionListener->finish();
338
339
        return $operationResultList;
340
    }
341
342
    /**
343
     * Loads and returns the list of synchronizations from the vault.
344
     *
345
     * @return SynchronizationList
346
     */
347
    public function loadSynchronizationList(): SynchronizationList
348
    {
349
        $storageDriver = $this->getStorageDriver();
350
        $list = null;
0 ignored issues
show
Unused Code introduced by
$list is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
351
352
        if ($storageDriver->exists(static::SYNCHRONIZATION_LIST_FILE_NAME))
353
        {
354
            $stream = $storageDriver->getReadStream(static::SYNCHRONIZATION_LIST_FILE_NAME);
355
356
            stream_filter_append($stream, 'zlib.inflate');
357
358
            $list = $this->readSynchronizationListFromStream($stream);
359
360
            fclose($stream);
361
362
            return $list;
363
        }
364
365
        return new SynchronizationList();
366
    }
367
368
    /**
369
     * Restores the local state at the given revision from the vault.
370
     *
371
     * @param int $revision
372
     * @param SynchronizationProgressListenerInterface $progressionListener
373
     *
374
     * @return OperationResultList
375
     * @throws Exception
376
     */
377
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
378
    {
379
        return $this->doRestore($revision, $progressionListener);
380
    }
381
382
    /**
383
     * @param string $targetPath
384
     * @param int $revision
385
     * @param SynchronizationProgressListenerInterface|null $progressListener
386
     *
387
     * @return OperationResultList
388
     * @throws \Exception
389
     */
390
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
391
    {
392
        return $this->doRestore($revision, $progressListener, true, $targetPath);
393
    }
394
395
    protected function doBuildLocalIndex(string $path = null): Index
396
    {
397
        $finder = new Finder();
398
        $finder->in($path ?: $this->configuration->getLocalPath());
399
        $finder->ignoreDotFiles(false);
400
        $finder->ignoreVCS(true);
401
        $finder->exclude(static::METADATA_DIRECTORY_NAME);
402
        $finder->notPath('archivr.json');
403
404
        foreach ($this->configuration->getExclusions() as $path)
405
        {
406
            $finder->notPath($path);
407
        }
408
409
        $index = new Index();
410
411
        foreach ($finder->directories() as $fileInfo)
412
        {
413
            /** @var SplFileInfo $fileInfo */
414
415
            $index->addObject(IndexObject::fromPath($this->configuration->getLocalPath(), $fileInfo->getRelativePathname()));
416
        }
417
418
        foreach ($finder->files() as $fileInfo)
419
        {
420
            /** @var SplFileInfo $fileInfo */
421
422
            $index->addObject(IndexObject::fromPath($this->configuration->getLocalPath(), $fileInfo->getRelativePathname()));
423
        }
424
425
        return $index;
426
    }
427
428
    protected function doLoadRemoteIndex(int $revision, SynchronizationList $synchronizationList = null)
429
    {
430
        if ($synchronizationList === null)
431
        {
432
            $synchronizationList = $this->loadSynchronizationList();
433
        }
434
435
        $synchronization = $synchronizationList->getSynchronizationByRevision($revision);
436
437
        if (!$synchronization)
438
        {
439
            return null;
440
        }
441
442
        $index = null;
443
444
        if ($this->storageDriver->exists($synchronization->getBlobId()))
445
        {
446
            $stream = $this->storageDriver->getReadStream($synchronization->getBlobId());
447
448
            stream_filter_append($stream, 'zlib.inflate');
449
450
            $index = $this->readIndexFromStream($stream);
451
452
            fclose($stream);
453
        }
454
455
        return $index;
456
    }
457
458
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null)
459
    {
460
        $localIndex = $localIndex ?: $this->buildLocalIndex();
461
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
462
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
463
464
        if ($remoteIndex === null)
465
        {
466
            return $localIndex;
467
        }
468
469
        return $this->getIndexMerger()->merge($this->getConflictHandler(), $remoteIndex, $localIndex, $lastLocalIndex);
470
    }
471
472
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
473
    {
474
        if ($progressionListener === null)
475
        {
476
            $progressionListener = new DummySynchronizationProgressListener();
477
        }
478
479
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
480
        {
481
            throw new Exception('Failed to acquire lock.');
482
        }
483
484 View Code Duplication
        if ($revision === null)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
485
        {
486
            $synchronizationList = $this->loadSynchronizationList();
487
488
            if (!$synchronizationList->getLastSynchronization())
489
            {
490
                throw new Exception('No revision to restore from.');
491
            }
492
493
            $revision = $synchronizationList->getLastSynchronization()->getRevision();
494
        }
495
496
        $remoteIndex = $this->loadRemoteIndex($revision);
497
498
        if ($remoteIndex === null)
499
        {
500
            throw new Exception("Unknown revision: {$revision}");
501
        }
502
503
        $targetPath = $targetPath ?: $this->configuration->getLocalPath();
504
505
        $localIndex = $this->doBuildLocalIndex($targetPath);
506
507
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex, $remoteIndex);
508
509
        $operationResultList = new OperationResultList();
510
511
        // operation count +
512
        // save merged index as last local index +
513
        // release lock
514
        $progressionListener->start(count($operationList) + 2);
515
516 View Code Duplication
        foreach ($operationList as $operation)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
517
        {
518
            /** @var OperationInterface $operation */
519
520
            $success = $operation->execute($targetPath, $this->getStorageDriver());
521
522
            $operationResult = new OperationResult($operation, $success);
523
            $operationResultList->addOperationResult($operationResult);
524
525
            $progressionListener->advance();
526
        }
527
528
        if (!$skipLastLocalIndexUpdate)
529
        {
530
            $this->writeIndexToFile($remoteIndex, $this->getLastLocalIndexFilePath());
531
        }
532
533
        $progressionListener->advance();
534
535
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
536
        {
537
            throw new Exception('Failed to release lock.');
538
        }
539
540
        $progressionListener->advance();
541
        $progressionListener->finish();
542
543
        return $operationResultList;
544
    }
545
546
    protected function readIndexFromStream($stream): Index
547
    {
548
        if (!is_resource($stream))
549
        {
550
            throw new Exception();
551
        }
552
553
        $index = new Index();
554
555
        while (($row = fgetcsv($stream)) !== false)
556
        {
557
            $index->addObject(IndexObject::fromIndexRecord($row));
558
        }
559
560
        return $index;
561
    }
562
563
    protected function writeIndexToFile(Index $index, string $path)
564
    {
565
        $stream = fopen($path, 'wb');
566
567
        foreach ($index as $object)
568
        {
569
            /** @var IndexObject $object */
570
571
            if (fputcsv($stream, $object->getIndexRecord()) === false)
572
            {
573
                throw new Exception();
574
            }
575
        }
576
577
        fclose($stream);
578
    }
579
580
    protected function readSynchronizationListFromStream($stream): SynchronizationList
581
    {
582
        if (!is_resource($stream))
583
        {
584
            throw new Exception();
585
        }
586
587
        $list = new SynchronizationList();
588
589
        while (($row = fgetcsv($stream)) !== false)
590
        {
591
            $list->addSynchronization(Synchronization::fromRecord($row));
592
        }
593
594
        return $list;
595
    }
596
597
    protected function writeSynchronizationListToTemporaryFile(SynchronizationList $synchronizationList): string
598
    {
599
        $path = tempnam(sys_get_temp_dir(), 'synchronizationList');
600
        $stream = fopen($path, 'wb');
601
602
        foreach ($synchronizationList as $synchronization)
603
        {
604
            /** @var Synchronization $synchronization */
605
606
            if (fputcsv($stream, $synchronization->getRecord()) === false)
607
            {
608
                throw new Exception();
609
            }
610
        }
611
612
        fclose($stream);
613
614
        return $path;
615
    }
616
617
    protected function generateNewBlobId(): string
618
    {
619
        do
620
        {
621
            $blobId = Uuid::uuid4()->toString();
622
        }
623
        while ($this->storageDriver->exists($blobId));
624
625
        return $blobId;
626
    }
627
628
    protected function initMetadataDirectory(): string
629
    {
630
        $path = $this->configuration->getLocalPath() . static::METADATA_DIRECTORY_NAME;
631
632
        if (!is_dir($path))
633
        {
634
            if (!mkdir($path))
635
            {
636
                throw new Exception();
637
            }
638
        }
639
640
        return $path . DIRECTORY_SEPARATOR;
641
    }
642
643
    protected function getLastLocalIndexFilePath(): string
644
    {
645
        return $this->initMetadataDirectory() . sprintf('lastLocalIndex-%s', $this->vaultConfiguration->getTitle());
646
    }
647
}
648