Completed
Push — master ( a8a6e5...b4d61a )
by Arne
04:19
created

Vault::getStorageDriver()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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