Completed
Push — master ( fa0d52...fef7a6 )
by Arne
01:59
created

Vault::getStorageDriverFactory()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.6666
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 0
1
<?php
2
3
namespace Archivr;
4
5
use Archivr\LockAdapter\LockAdapterFactory;
6
use Archivr\StorageDriver\StorageDriverFactory;
7
use Archivr\StorageDriver\StorageDriverInterface;
8
use Archivr\Exception\Exception;
9
use Archivr\IndexMerger\IndexMergerInterface;
10
use Archivr\IndexMerger\StandardIndexMerger;
11
use Archivr\LockAdapter\LockAdapterInterface;
12
use Archivr\OperationListBuilder\OperationListBuilderInterface;
13
use Archivr\OperationListBuilder\StandardOperationListBuilder;
14
use Archivr\SynchronizationProgressListener\DummySynchronizationProgressListener;
15
use Archivr\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
16
use Ramsey\Uuid\Uuid;
17
use Symfony\Component\Finder\Finder;
18
use Symfony\Component\Finder\SplFileInfo;
19
use Archivr\Operation\OperationInterface;
20
21
class Vault
22
{
23
    const METADATA_DIRECTORY_NAME = '.archivr';
24
    const SYNCHRONIZATION_LIST_FILE_NAME = 'index';
25
    const LOCK_SYNC = 'sync';
26
27
    /**
28
     * @var VaultConfiguration
29
     */
30
    protected $vaultConfiguration;
31
32
    /**
33
     * @var Configuration
34
     */
35
    protected $configuration;
36
37
    /**
38
     * @var StorageDriverFactory
39
     */
40
    protected $storageDriverFactory;
41
42
    /**
43
     * @var LockAdapterFactory
44
     */
45
    protected $lockAdapterFactory;
46
47
    /**
48
     * @var StorageDriverInterface
49
     */
50
    protected $storageDriver;
51
52
    /**
53
     * @var LockAdapterInterface
54
     */
55
    protected $lockAdapter;
56
57
    /**
58
     * @var IndexMergerInterface
59
     */
60
    protected $indexMerger;
61
62
    /**
63
     * @var OperationListBuilderInterface
64
     */
65
    protected $operationListBuilder;
66
67
    public function __construct(Configuration $configuration, VaultConfiguration $vaultConfiguration)
68
    {
69
        $this->configuration = $configuration;
70
        $this->vaultConfiguration = $vaultConfiguration;
71
    }
72
73
    public function getConfiguration(): Configuration
74
    {
75
        return $this->configuration;
76
    }
77
78
    public function getVaultConfiguration(): VaultConfiguration
79
    {
80
        return $this->vaultConfiguration;
81
    }
82
83
    public function getStorageDriverFactory(): StorageDriverFactory
84
    {
85
        if ($this->storageDriverFactory === null)
86
        {
87
            $this->setStorageDriverFactory(new StorageDriverFactory());
88
        }
89
90
        return $this->storageDriverFactory;
91
    }
92
93
    public function setStorageDriverFactory(StorageDriverFactory $storageDriverFactory)
94
    {
95
        $this->storageDriverFactory = $storageDriverFactory;
96
    }
97
98
    public function getLockAdapterFactory(): LockAdapterFactory
99
    {
100
        if ($this->lockAdapterFactory === null)
101
        {
102
            $this->setLockAdapterFactory(new LockAdapterFactory());
103
        }
104
105
        return $this->lockAdapterFactory;
106
    }
107
108
    public function setLockAdapterFactory(LockAdapterFactory $lockAdapterFactory)
109
    {
110
        $this->lockAdapterFactory = $lockAdapterFactory;
111
    }
112
113
    public function setIndexMerger(IndexMergerInterface $indexMerger = null)
114
    {
115
        $this->indexMerger = $indexMerger;
116
117
        return $this;
118
    }
119
120
    public function getIndexMerger(): IndexMergerInterface
121
    {
122
        if ($this->indexMerger === null)
123
        {
124
            $this->indexMerger = new StandardIndexMerger();
125
        }
126
127
        return $this->indexMerger;
128
    }
129
130
    public function getOperationListBuilder(): OperationListBuilderInterface
131
    {
132
        if ($this->operationListBuilder === null)
133
        {
134
            $this->operationListBuilder = new StandardOperationListBuilder();
135
        }
136
137
        return $this->operationListBuilder;
138
    }
139
140
    public function setOperationListBuilder(OperationListBuilderInterface $operationListBuilder = null): Vault
141
    {
142
        $this->operationListBuilder = $operationListBuilder;
143
144
        return $this;
145
    }
146
147
    public function getStorageDriver(): StorageDriverInterface
148
    {
149
        if ($this->storageDriver === null)
150
        {
151
            $this->storageDriver = $this->getStorageDriverFactory()->create(
152
                $this->vaultConfiguration->getStorageDriver(),
153
                $this->vaultConfiguration
154
            );
155
        }
156
157
        return $this->storageDriver;
158
    }
159
160
    public function getLockAdapter(): LockAdapterInterface
161
    {
162
        if ($this->lockAdapter === null)
163
        {
164
            $this->lockAdapter = $this->getLockAdapterFactory()->create(
165
                $this->vaultConfiguration->getLockAdapter(),
166
                $this->vaultConfiguration,
167
                $this->getStorageDriver()
168
            );
169
        }
170
171
        return $this->lockAdapter;
172
    }
173
174
    /**
175
     * Builds and returns an index representing the current local state.
176
     *
177
     * @return Index
178
     */
179
    public function buildLocalIndex(): Index
180
    {
181
        return $this->doBuildLocalIndex();
182
    }
183
184
    /**
185
     * Reads and returns the index representing the local state on the last synchronization.
186
     *
187
     * @return Index
188
     * @throws Exception
189
     */
190
    public function loadLastLocalIndex()
191
    {
192
        $index = null;
193
        $path = $this->getLastLocalIndexFilePath();
194
195
        if (is_file($path))
196
        {
197
            $stream = fopen($path, 'rb');
198
199
            $index = $this->readIndexFromStream($stream);
200
201
            fclose($stream);
202
        }
203
204
        return $index;
205
    }
206
207
    /**
208
     * Reads and returns the current remote index.
209
     *
210
     * @param int $revision Revision to load. Defaults to the last revision.
211
     *
212
     * @return Index
213
     */
214
    public function loadRemoteIndex(int $revision = null)
215
    {
216
        $list = null;
217
218 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...
219
        {
220
            $list = $this->loadSynchronizationList();
221
222
            if (!$list->getLastSynchronization())
223
            {
224
                return null;
225
            }
226
227
            $revision = $list->getLastSynchronization()->getRevision();
228
        }
229
230
        return $this->doLoadRemoteIndex($revision, $list);
231
    }
232
233
    /**
234
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
235
     *
236
     * @return Index
237
     */
238
    public function buildMergedIndex(): Index
239
    {
240
        return $this->doBuildMergedIndex();
241
    }
242
243
    /**
244
     * Returns ordered list of operations required to synchronize the vault with the local path.
245
     * In addition to the object specific operations contained in the returned OperationList additional operations
246
     * might be necessary like index updates that do not belong to specific index objects.
247
     *
248
     * @return OperationList
249
     */
250
    public function getOperationList(): OperationList
251
    {
252
        $localIndex = $this->buildLocalIndex();
253
        $lastLocalIndex = $this->loadLastLocalIndex();
254
        $remoteIndex = $this->loadRemoteIndex();
255
256
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
257
258
        return $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
259
    }
260
261
    /**
262
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
263
     *
264
     * @param int $newRevision
265
     * @param bool $preferLocal
266
     * @param SynchronizationProgressListenerInterface $progressionListener
267
     *
268
     * @return OperationResultList
269
     * @throws Exception
270
     */
271
    public function synchronize(int $newRevision = null, bool $preferLocal = false, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
272
    {
273
        if ($progressionListener === null)
274
        {
275
            $progressionListener = new DummySynchronizationProgressListener();
276
        }
277
278
        $localIndex = $this->buildLocalIndex();
279
        $lastLocalIndex = $this->loadLastLocalIndex();
280
281
282
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
283
        {
284
            throw new Exception('Failed to acquire lock.');
285
        }
286
287
288
        $synchronizationList = $this->loadSynchronizationList();
289
        $lastSynchronization = $synchronizationList->getLastSynchronization();
290
291
        if ($lastSynchronization)
292
        {
293
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
294
            $remoteIndex = $this->doLoadRemoteIndex($lastSynchronization->getRevision(), $synchronizationList);
295
        }
296
        else
297
        {
298
            $newRevision = $newRevision ?: 1;
299
            $remoteIndex = null;
300
        }
301
302
        $synchronization = new Synchronization($newRevision, $this->generateNewBlobId(), new \DateTime(), $this->configuration->getIdentity());
303
        $synchronizationList->addSynchronization($synchronization);
304
305
        // don't merge indices but just use local
306
        if ($preferLocal)
307
        {
308
            $mergedIndex = $localIndex;
309
        }
310
311
        // compute merged index
312
        else
313
        {
314
            $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
315
        }
316
317
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
318
319
        $operationResultList = new OperationResultList();
320
321
        // operation count +
322
        // merged index write +
323
        // copy merged index to vault +
324
        // save merged index as last local index +
325
        // upload synchronization list +
326
        // release lock
327
        $progressionListener->start(count($operationList) + 5);
328
329 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...
330
        {
331
            /** @var OperationInterface $operation */
332
333
            $success = $operation->execute($this->configuration->getLocalPath(), $this->getStorageDriver());
334
335
            $operationResult = new OperationResult($operation, $success);
336
            $operationResultList->addOperationResult($operationResult);
337
338
            $progressionListener->advance();
339
        }
340
341
        // dump new index
342
        $mergedIndexFilePath = tempnam(sys_get_temp_dir(), 'index');
343
        $this->writeIndexToFile($mergedIndex, $mergedIndexFilePath);
344
345
        $progressionListener->advance();
346
347
        // upload new index
348
        $readStream = fopen($mergedIndexFilePath, 'rb');
349
        $compressionFilter = stream_filter_append($readStream, 'zlib.deflate');
350
        $this->storageDriver->writeStream($synchronization->getBlobId(), $readStream);
351
        rewind($readStream);
352
        stream_filter_remove($compressionFilter);
353
354
        $progressionListener->advance();
355
356
        // save new index locally
357
        $writeStream = fopen($this->getLastLocalIndexFilePath(), 'wb');
358
        stream_copy_to_stream($readStream, $writeStream);
359
        fclose($writeStream);
360
        fclose($readStream);
361
362
        $progressionListener->advance();
363
364
        // upload new synchronization list
365
        $synchronizationListFilePath = $this->writeSynchronizationListToTemporaryFile($synchronizationList);
366
        $readStream = fopen($synchronizationListFilePath, 'rb');
367
        stream_filter_append($readStream, 'zlib.deflate');
368
        $this->storageDriver->writeStream(static::SYNCHRONIZATION_LIST_FILE_NAME, $readStream);
369
        fclose($readStream);
370
371
        // release lock
372
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
373
        {
374
            throw new Exception('Failed to release lock.');
375
        }
376
377
        $progressionListener->advance();
378
        $progressionListener->finish();
379
380
        return $operationResultList;
381
    }
382
383
    /**
384
     * Loads and returns the list of synchronizations from the vault.
385
     *
386
     * @return SynchronizationList
387
     */
388
    public function loadSynchronizationList(): SynchronizationList
389
    {
390
        $storageDriver = $this->getStorageDriver();
391
        $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...
392
393
        if ($storageDriver->exists(static::SYNCHRONIZATION_LIST_FILE_NAME))
394
        {
395
            $stream = $storageDriver->getReadStream(static::SYNCHRONIZATION_LIST_FILE_NAME);
396
397
            stream_filter_append($stream, 'zlib.inflate');
398
399
            $list = $this->readSynchronizationListFromStream($stream);
400
401
            fclose($stream);
402
403
            return $list;
404
        }
405
406
        return new SynchronizationList();
407
    }
408
409
    /**
410
     * Restores the local state at the given revision from the vault.
411
     *
412
     * @param int $revision
413
     * @param SynchronizationProgressListenerInterface $progressionListener
414
     *
415
     * @return OperationResultList
416
     * @throws Exception
417
     */
418
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
419
    {
420
        return $this->doRestore($revision, $progressionListener);
421
    }
422
423
    /**
424
     * @param string $targetPath
425
     * @param int $revision
426
     * @param SynchronizationProgressListenerInterface|null $progressListener
427
     *
428
     * @return OperationResultList
429
     * @throws \Exception
430
     */
431
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
432
    {
433
        return $this->doRestore($revision, $progressListener, true, $targetPath);
434
    }
435
436
    protected function doBuildLocalIndex(string $path = null): Index
437
    {
438
        $finder = new Finder();
439
        $finder->in($path ?: $this->configuration->getLocalPath());
440
        $finder->ignoreDotFiles(false);
441
        $finder->ignoreVCS(true);
442
        $finder->exclude(static::METADATA_DIRECTORY_NAME);
443
        $finder->notPath('archivr.json');
444
445
        foreach ($this->configuration->getExclusions() as $path)
446
        {
447
            $finder->notPath($path);
448
        }
449
450
        $index = new Index();
451
452
        foreach ($finder->directories() as $fileInfo)
453
        {
454
            /** @var SplFileInfo $fileInfo */
455
456
            $index->addObject(IndexObject::fromPath($this->configuration->getLocalPath(), $fileInfo->getRelativePathname()));
457
        }
458
459
        foreach ($finder->files() as $fileInfo)
460
        {
461
            /** @var SplFileInfo $fileInfo */
462
463
            $index->addObject(IndexObject::fromPath($this->configuration->getLocalPath(), $fileInfo->getRelativePathname()));
464
        }
465
466
        return $index;
467
    }
468
469
    protected function doLoadRemoteIndex(int $revision, SynchronizationList $synchronizationList = null)
470
    {
471
        if ($synchronizationList === null)
472
        {
473
            $synchronizationList = $this->loadSynchronizationList();
474
        }
475
476
        $synchronization = $synchronizationList->getSynchronizationByRevision($revision);
477
478
        if (!$synchronization)
479
        {
480
            return null;
481
        }
482
483
        $index = null;
484
485
        if ($this->storageDriver->exists($synchronization->getBlobId()))
486
        {
487
            $stream = $this->storageDriver->getReadStream($synchronization->getBlobId());
488
489
            stream_filter_append($stream, 'zlib.inflate');
490
491
            $index = $this->readIndexFromStream($stream);
492
493
            fclose($stream);
494
        }
495
496
        return $index;
497
    }
498
499
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null)
500
    {
501
        $localIndex = $localIndex ?: $this->buildLocalIndex();
502
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
503
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
504
505
        if ($remoteIndex === null)
506
        {
507
            return $localIndex;
508
        }
509
510
        return $this->getIndexMerger()->merge($remoteIndex, $localIndex, $lastLocalIndex);
511
    }
512
513
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
514
    {
515
        if ($progressionListener === null)
516
        {
517
            $progressionListener = new DummySynchronizationProgressListener();
518
        }
519
520
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
521
        {
522
            throw new Exception('Failed to acquire lock.');
523
        }
524
525 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...
526
        {
527
            $synchronizationList = $this->loadSynchronizationList();
528
529
            if (!$synchronizationList->getLastSynchronization())
530
            {
531
                throw new Exception('No revision to restore from.');
532
            }
533
534
            $revision = $synchronizationList->getLastSynchronization()->getRevision();
535
        }
536
537
        $remoteIndex = $this->loadRemoteIndex($revision);
538
539
        if ($remoteIndex === null)
540
        {
541
            throw new Exception("Unknown revision: {$revision}");
542
        }
543
544
        $targetPath = $targetPath ?: $this->configuration->getLocalPath();
545
546
        $localIndex = $this->doBuildLocalIndex($targetPath);
547
548
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex, $remoteIndex);
549
550
        $operationResultList = new OperationResultList();
551
552
        // operation count +
553
        // save merged index as last local index +
554
        // release lock
555
        $progressionListener->start(count($operationList) + 2);
556
557 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...
558
        {
559
            /** @var OperationInterface $operation */
560
561
            $success = $operation->execute($targetPath, $this->getStorageDriver());
562
563
            $operationResult = new OperationResult($operation, $success);
564
            $operationResultList->addOperationResult($operationResult);
565
566
            $progressionListener->advance();
567
        }
568
569
        if (!$skipLastLocalIndexUpdate)
570
        {
571
            $this->writeIndexToFile($remoteIndex, $this->getLastLocalIndexFilePath());
572
        }
573
574
        $progressionListener->advance();
575
576
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
577
        {
578
            throw new Exception('Failed to release lock.');
579
        }
580
581
        $progressionListener->advance();
582
        $progressionListener->finish();
583
584
        return $operationResultList;
585
    }
586
587
    protected function readIndexFromStream($stream): Index
588
    {
589
        if (!is_resource($stream))
590
        {
591
            throw new Exception();
592
        }
593
594
        $index = new Index();
595
596
        while (($row = fgetcsv($stream)) !== false)
597
        {
598
            $index->addObject(IndexObject::fromIndexRecord($row));
599
        }
600
601
        return $index;
602
    }
603
604
    protected function writeIndexToFile(Index $index, string $path)
605
    {
606
        $stream = fopen($path, 'wb');
607
608
        foreach ($index as $object)
609
        {
610
            /** @var IndexObject $object */
611
612
            if (fputcsv($stream, $object->getIndexRecord()) === false)
613
            {
614
                throw new Exception();
615
            }
616
        }
617
618
        fclose($stream);
619
    }
620
621
    protected function readSynchronizationListFromStream($stream): SynchronizationList
622
    {
623
        if (!is_resource($stream))
624
        {
625
            throw new Exception();
626
        }
627
628
        $list = new SynchronizationList();
629
630
        while (($row = fgetcsv($stream)) !== false)
631
        {
632
            $list->addSynchronization(Synchronization::fromRecord($row));
633
        }
634
635
        return $list;
636
    }
637
638
    protected function writeSynchronizationListToTemporaryFile(SynchronizationList $synchronizationList): string
639
    {
640
        $path = tempnam(sys_get_temp_dir(), 'synchronizationList');
641
        $stream = fopen($path, 'wb');
642
643
        foreach ($synchronizationList as $synchronization)
644
        {
645
            /** @var Synchronization $synchronization */
646
647
            if (fputcsv($stream, $synchronization->getRecord()) === false)
648
            {
649
                throw new Exception();
650
            }
651
        }
652
653
        fclose($stream);
654
655
        return $path;
656
    }
657
658
    protected function generateNewBlobId(): string
659
    {
660
        do
661
        {
662
            $blobId = Uuid::uuid4()->toString();
663
        }
664
        while ($this->storageDriver->exists($blobId));
665
666
        return $blobId;
667
    }
668
669
    protected function initMetadataDirectory(): string
670
    {
671
        $path = $this->configuration->getLocalPath() . static::METADATA_DIRECTORY_NAME;
672
673
        if (!is_dir($path))
674
        {
675
            if (!mkdir($path))
676
            {
677
                throw new Exception();
678
            }
679
        }
680
681
        return $path . DIRECTORY_SEPARATOR;
682
    }
683
684
    protected function getLastLocalIndexFilePath(): string
685
    {
686
        return $this->initMetadataDirectory() . sprintf('lastLocalIndex-%s', $this->vaultConfiguration->getTitle());
687
    }
688
}
689