Completed
Push — master ( c5a3b4...fa0d52 )
by Arne
01:49
created

Vault::getOperationList()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 6
nc 1
nop 0
1
<?php
2
3
namespace Archivr;
4
5
use Archivr\StorageDriver\StorageDriverInterface;
6
use Archivr\Exception\Exception;
7
use Archivr\IndexMerger\IndexMergerInterface;
8
use Archivr\IndexMerger\StandardIndexMerger;
9
use Archivr\LockAdapter\StorageBasedLockAdapter;
10
use Archivr\LockAdapter\LockAdapterInterface;
11
use Archivr\OperationListBuilder\OperationListBuilderInterface;
12
use Archivr\OperationListBuilder\StandardOperationListBuilder;
13
use Archivr\SynchronizationProgressListener\DummySynchronizationProgressListener;
14
use Archivr\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
15
use Ramsey\Uuid\Uuid;
16
use Symfony\Component\Finder\Finder;
17
use Symfony\Component\Finder\SplFileInfo;
18
use Archivr\Operation\OperationInterface;
19
20
class Vault
21
{
22
    use TildeExpansionTrait;
23
24
    const METADATA_DIRECTORY_NAME = '.archivr';
25
    const SYNCHRONIZATION_LIST_FILE_NAME = 'index';
26
    const LOCK_SYNC = 'sync';
27
28
    /**
29
     * @var string
30
     */
31
    protected $title;
32
33
    /**
34
     * @var StorageDriverInterface
35
     */
36
    protected $storageDriver;
37
38
    /**
39
     * @var string
40
     */
41
    protected $localPath;
42
43
    /**
44
     * @var LockAdapterInterface
45
     */
46
    protected $lockAdapter;
47
48
    /**
49
     * @var IndexMergerInterface
50
     */
51
    protected $indexMerger;
52
53
    /**
54
     * @var OperationListBuilderInterface
55
     */
56
    protected $operationListBuilder;
57
58
    /**
59
     * @var string[]
60
     */
61
    protected $exclusions = [];
62
63
    /**
64
     * @var string
65
     */
66
    protected $identity;
67
68
69
    public function __construct(string $title, string $localPath, StorageDriverInterface $storageDriver)
70
    {
71
        $this->title = $title;
72
        $this->storageDriver = $storageDriver;
73
        $this->localPath = rtrim($this->expandTildePath($localPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
74
    }
75
76
    public function getTitle(): string
77
    {
78
        return $this->title;
79
    }
80
81
    public function getLocalPath(): string
82
    {
83
        return $this->localPath;
84
    }
85
86
    public function setIndexMerger(IndexMergerInterface $indexMerger = null)
87
    {
88
        $this->indexMerger = $indexMerger;
89
90
        return $this;
91
    }
92
93
    public function getIndexMerger(): IndexMergerInterface
94
    {
95
        if ($this->indexMerger === null)
96
        {
97
            $this->indexMerger = new StandardIndexMerger();
98
        }
99
100
        return $this->indexMerger;
101
    }
102
103
    public function getOperationListBuilder(): OperationListBuilderInterface
104
    {
105
        if ($this->operationListBuilder === null)
106
        {
107
            $this->operationListBuilder = new StandardOperationListBuilder();
108
        }
109
110
        return $this->operationListBuilder;
111
    }
112
113
    public function setOperationListBuilder(OperationListBuilderInterface $operationListBuilder = null): Vault
114
    {
115
        $this->operationListBuilder = $operationListBuilder;
116
117
        return $this;
118
    }
119
120
    public function setLockAdapter(LockAdapterInterface $lockAdapter = null): Vault
121
    {
122
        $this->lockAdapter = $lockAdapter;
123
124
        return $this;
125
    }
126
127
    public function getLockAdapter(): LockAdapterInterface
128
    {
129
        if ($this->lockAdapter === null)
130
        {
131
            $this->lockAdapter = new StorageBasedLockAdapter($this->storageDriver);
132
        }
133
134
        return $this->lockAdapter;
135
    }
136
137
    public function getStorageDriver(): StorageDriverInterface
138
    {
139
        return $this->storageDriver;
140
    }
141
142
    public function getExclusions(): array
143
    {
144
        return $this->exclusions;
145
    }
146
147
    public function addExclusion(string $path): Vault
148
    {
149
        $this->exclusions[] = $path;
150
151
        return $this;
152
    }
153
154
    public function setExclusions(array $paths): Vault
155
    {
156
        $this->exclusions = array_values($paths);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_values($paths) of type array<integer,?> is incompatible with the declared type array<integer,string> of property $exclusions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
157
158
        return $this;
159
    }
160
161
    public function getIdentity(): string
162
    {
163
        return $this->identity;
164
    }
165
166
    public function setIdentity(string $identity = null): Vault
167
    {
168
        $this->identity = $identity;
169
170
        return $this;
171
    }
172
173
    /**
174
     * Builds and returns an index representing the current local state.
175
     *
176
     * @return Index
177
     */
178
    public function buildLocalIndex(): Index
179
    {
180
        $finder = new Finder();
181
        $finder->in($this->localPath);
182
        $finder->ignoreDotFiles(false);
183
        $finder->ignoreVCS(true);
184
        $finder->exclude(static::METADATA_DIRECTORY_NAME);
185
        $finder->notPath('archivr.json');
186
187
        foreach ($this->exclusions as $path)
188
        {
189
            $finder->notPath($path);
190
        }
191
192
        $index = new Index();
193
194
        foreach ($finder->directories() as $fileInfo)
195
        {
196
            /** @var SplFileInfo $fileInfo */
197
198
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
199
        }
200
201
        foreach ($finder->files() as $fileInfo)
202
        {
203
            /** @var SplFileInfo $fileInfo */
204
205
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
206
        }
207
208
        return $index;
209
    }
210
211
    /**
212
     * Reads and returns the index representing the local state on the last synchronization.
213
     *
214
     * @return Index
215
     * @throws Exception
216
     */
217
    public function loadLastLocalIndex()
218
    {
219
        $index = null;
220
        $path = $this->getLastLocalIndexFilePath();
221
222
        if (is_file($path))
223
        {
224
            $stream = fopen($path, 'rb');
225
226
            $index = $this->readIndexFromStream($stream);
227
228
            fclose($stream);
229
        }
230
231
        return $index;
232
    }
233
234
    /**
235
     * Reads and returns the current remote index.
236
     *
237
     * @param int $revision Revision to load. Defaults to the last revision.
238
     *
239
     * @return Index
240
     */
241
    public function loadRemoteIndex(int $revision = null)
242
    {
243
        $list = null;
244
245 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...
246
        {
247
            $list = $this->loadSynchronizationList();
248
249
            if (!$list->getLastSynchronization())
250
            {
251
                return null;
252
            }
253
254
            $revision = $list->getLastSynchronization()->getRevision();
255
        }
256
257
        return $this->doLoadRemoteIndex($revision, $list);
258
    }
259
260
    /**
261
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
262
     *
263
     * @return Index
264
     */
265
    public function buildMergedIndex(): Index
266
    {
267
        return $this->doBuildMergedIndex();
268
    }
269
270
    /**
271
     * Returns ordered list of operations required to synchronize the vault with the local path.
272
     * In addition to the object specific operations contained in the returned OperationList additional operations
273
     * might be necessary like index updates that do not belong to specific index objects.
274
     *
275
     * @return OperationList
276
     */
277
    public function getOperationList(): OperationList
278
    {
279
        $localIndex = $this->buildLocalIndex();
280
        $lastLocalIndex = $this->loadLastLocalIndex();
281
        $remoteIndex = $this->loadRemoteIndex();
282
283
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
284
285
        return $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
286
    }
287
288
    /**
289
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
290
     *
291
     * @param int $newRevision
292
     * @param bool $preferLocal
293
     * @param SynchronizationProgressListenerInterface $progressionListener
294
     *
295
     * @return OperationResultList
296
     * @throws Exception
297
     */
298
    public function synchronize(int $newRevision = null, bool $preferLocal = false, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
299
    {
300
        if ($progressionListener === null)
301
        {
302
            $progressionListener = new DummySynchronizationProgressListener();
303
        }
304
305
        $localIndex = $this->buildLocalIndex();
306
        $lastLocalIndex = $this->loadLastLocalIndex();
307
308
309
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
310
        {
311
            throw new Exception('Failed to acquire lock.');
312
        }
313
314
315
        $synchronizationList = $this->loadSynchronizationList();
316
        $lastSynchronization = $synchronizationList->getLastSynchronization();
317
318
        if ($lastSynchronization)
319
        {
320
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
321
            $remoteIndex = $this->doLoadRemoteIndex($lastSynchronization->getRevision(), $synchronizationList);
322
        }
323
        else
324
        {
325
            $newRevision = $newRevision ?: 1;
326
            $remoteIndex = null;
327
        }
328
329
        $synchronization = new Synchronization($newRevision, $this->generateNewBlobId(), new \DateTime(), $this->identity);
330
        $synchronizationList->addSynchronization($synchronization);
331
332
        // don't merge indices but just use local
333
        if ($preferLocal)
334
        {
335
            $mergedIndex = $localIndex;
336
        }
337
338
        // compute merged index
339
        else
340
        {
341
            $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
342
        }
343
344
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
345
346
        $operationResultList = new OperationResultList();
347
348
        // operation count +
349
        // merged index write +
350
        // copy merged index to vault +
351
        // save merged index as last local index +
352
        // upload synchronization list +
353
        // release lock
354
        $progressionListener->start(count($operationList) + 5);
355
356 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...
357
        {
358
            /** @var OperationInterface $operation */
359
360
            $success = $operation->execute($this->getLocalPath(), $this->getStorageDriver());
361
362
            $operationResult = new OperationResult($operation, $success);
363
            $operationResultList->addOperationResult($operationResult);
364
365
            $progressionListener->advance();
366
        }
367
368
        // dump new index
369
        $mergedIndexFilePath = tempnam(sys_get_temp_dir(), 'index');
370
        $this->writeIndexToFile($mergedIndex, $mergedIndexFilePath);
371
372
        $progressionListener->advance();
373
374
        // upload new index
375
        $readStream = fopen($mergedIndexFilePath, 'rb');
376
        $compressionFilter = stream_filter_append($readStream, 'zlib.deflate');
377
        $this->storageDriver->writeStream($synchronization->getBlobId(), $readStream);
378
        rewind($readStream);
379
        stream_filter_remove($compressionFilter);
380
381
        $progressionListener->advance();
382
383
        // save new index locally
384
        $writeStream = fopen($this->getLastLocalIndexFilePath(), 'wb');
385
        stream_copy_to_stream($readStream, $writeStream);
386
        fclose($writeStream);
387
        fclose($readStream);
388
389
        $progressionListener->advance();
390
391
        // upload new synchronization list
392
        $synchronizationListFilePath = $this->writeSynchronizationListToTemporaryFile($synchronizationList);
393
        $readStream = fopen($synchronizationListFilePath, 'rb');
394
        stream_filter_append($readStream, 'zlib.deflate');
395
        $this->storageDriver->writeStream(static::SYNCHRONIZATION_LIST_FILE_NAME, $readStream);
396
        fclose($readStream);
397
398
        // release lock
399
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
400
        {
401
            throw new Exception('Failed to release lock.');
402
        }
403
404
        $progressionListener->advance();
405
        $progressionListener->finish();
406
407
        return $operationResultList;
408
    }
409
410
    /**
411
     * Loads and returns the list of synchronizations from the vault.
412
     *
413
     * @return SynchronizationList
414
     */
415
    public function loadSynchronizationList(): SynchronizationList
416
    {
417
        $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...
418
419
        if ($this->storageDriver->exists(static::SYNCHRONIZATION_LIST_FILE_NAME))
420
        {
421
            $stream = $this->storageDriver->getReadStream(static::SYNCHRONIZATION_LIST_FILE_NAME);
422
423
            stream_filter_append($stream, 'zlib.inflate');
424
425
            $list = $this->readSynchronizationListFromStream($stream);
426
427
            fclose($stream);
428
429
            return $list;
430
        }
431
432
        return new SynchronizationList();
433
    }
434
435
    /**
436
     * Restores the local state at the given revision from the vault.
437
     *
438
     * @param int $revision
439
     * @param SynchronizationProgressListenerInterface $progressionListener
440
     *
441
     * @return OperationResultList
442
     * @throws Exception
443
     */
444
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
445
    {
446
        return $this->doRestore($revision, $progressionListener);
447
    }
448
449
    /**
450
     * @param string $targetPath
451
     * @param int $revision
452
     * @param SynchronizationProgressListenerInterface|null $progressListener
453
     *
454
     * @return OperationResultList
455
     * @throws \Exception
456
     */
457
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
458
    {
459
        $originalLocalPath = $this->localPath;
460
        $this->localPath =  rtrim($this->expandTildePath($targetPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
461
462
        try
463
        {
464
            return $this->doRestore($revision, $progressListener, true);
465
        }
466
        catch (\Exception $exception)
467
        {
468
            throw $exception;
469
        }
470
        finally
471
        {
472
            $this->localPath = $originalLocalPath;
473
        }
474
    }
475
476
    protected function doLoadRemoteIndex(int $revision, SynchronizationList $synchronizationList = null)
477
    {
478
        if ($synchronizationList === null)
479
        {
480
            $synchronizationList = $this->loadSynchronizationList();
481
        }
482
483
        $synchronization = $synchronizationList->getSynchronizationByRevision($revision);
484
485
        if (!$synchronization)
486
        {
487
            return null;
488
        }
489
490
        $index = null;
491
492
        if ($this->storageDriver->exists($synchronization->getBlobId()))
493
        {
494
            $stream = $this->storageDriver->getReadStream($synchronization->getBlobId());
495
496
            stream_filter_append($stream, 'zlib.inflate');
497
498
            $index = $this->readIndexFromStream($stream);
499
500
            fclose($stream);
501
        }
502
503
        return $index;
504
    }
505
506
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null)
507
    {
508
        $localIndex = $localIndex ?: $this->buildLocalIndex();
509
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
510
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
511
512
        if ($remoteIndex === null)
513
        {
514
            return $localIndex;
515
        }
516
517
        return $this->getIndexMerger()->merge($remoteIndex, $localIndex, $lastLocalIndex);
518
    }
519
520
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false): OperationResultList
521
    {
522
        if ($progressionListener === null)
523
        {
524
            $progressionListener = new DummySynchronizationProgressListener();
525
        }
526
527
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
528
        {
529
            throw new Exception('Failed to acquire lock.');
530
        }
531
532 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...
533
        {
534
            $synchronizationList = $this->loadSynchronizationList();
535
536
            if (!$synchronizationList->getLastSynchronization())
537
            {
538
                throw new Exception('No revision to restore from.');
539
            }
540
541
            $revision = $synchronizationList->getLastSynchronization()->getRevision();
542
        }
543
544
        $remoteIndex = $this->loadRemoteIndex($revision);
545
546
        if ($remoteIndex === null)
547
        {
548
            throw new Exception("Unknown revision: {$revision}");
549
        }
550
551
        $localIndex = $this->buildLocalIndex();
552
553
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex, $remoteIndex);
554
555
        $operationResultList = new OperationResultList();
556
557
        // operation count +
558
        // save merged index as last local index +
559
        // release lock
560
        $progressionListener->start(count($operationList) + 2);
561
562 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...
563
        {
564
            /** @var OperationInterface $operation */
565
566
            $success = $operation->execute($this->getLocalPath(), $this->getStorageDriver());
567
568
            $operationResult = new OperationResult($operation, $success);
569
            $operationResultList->addOperationResult($operationResult);
570
571
            $progressionListener->advance();
572
        }
573
574
        if (!$skipLastLocalIndexUpdate)
575
        {
576
            $this->writeIndexToFile($remoteIndex, $this->getLastLocalIndexFilePath());
577
        }
578
579
        $progressionListener->advance();
580
581
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
582
        {
583
            throw new Exception('Failed to release lock.');
584
        }
585
586
        $progressionListener->advance();
587
        $progressionListener->finish();
588
589
        return $operationResultList;
590
    }
591
592
    protected function readIndexFromStream($stream): Index
593
    {
594
        if (!is_resource($stream))
595
        {
596
            throw new Exception();
597
        }
598
599
        $index = new Index();
600
601
        while (($row = fgetcsv($stream)) !== false)
602
        {
603
            $index->addObject(IndexObject::fromIndexRecord($row));
604
        }
605
606
        return $index;
607
    }
608
609
    protected function writeIndexToFile(Index $index, string $path)
610
    {
611
        $stream = fopen($path, 'wb');
612
613
        foreach ($index as $object)
614
        {
615
            /** @var IndexObject $object */
616
617
            if (fputcsv($stream, $object->getIndexRecord()) === false)
618
            {
619
                throw new Exception();
620
            }
621
        }
622
623
        fclose($stream);
624
    }
625
626
    protected function readSynchronizationListFromStream($stream): SynchronizationList
627
    {
628
        if (!is_resource($stream))
629
        {
630
            throw new Exception();
631
        }
632
633
        $list = new SynchronizationList();
634
635
        while (($row = fgetcsv($stream)) !== false)
636
        {
637
            $list->addSynchronization(Synchronization::fromRecord($row));
638
        }
639
640
        return $list;
641
    }
642
643
    protected function writeSynchronizationListToTemporaryFile(SynchronizationList $synchronizationList): string
644
    {
645
        $path = tempnam(sys_get_temp_dir(), 'synchronizationList');
646
        $stream = fopen($path, 'wb');
647
648
        foreach ($synchronizationList as $synchronization)
649
        {
650
            /** @var Synchronization $synchronization */
651
652
            if (fputcsv($stream, $synchronization->getRecord()) === false)
653
            {
654
                throw new Exception();
655
            }
656
        }
657
658
        fclose($stream);
659
660
        return $path;
661
    }
662
663
    protected function generateNewBlobId(): string
664
    {
665
        do
666
        {
667
            $blobId = Uuid::uuid4()->toString();
668
        }
669
        while ($this->storageDriver->exists($blobId));
670
671
        return $blobId;
672
    }
673
674
    protected function initMetadataDirectory(): string
675
    {
676
        $path = $this->localPath . static::METADATA_DIRECTORY_NAME;
677
678
        if (!is_dir($path))
679
        {
680
            if (!mkdir($path))
681
            {
682
                throw new Exception();
683
            }
684
        }
685
686
        return $path . DIRECTORY_SEPARATOR;
687
    }
688
689
    protected function getLastLocalIndexFilePath(): string
690
    {
691
        return $this->initMetadataDirectory() . sprintf('lastLocalIndex-%s', $this->getTitle());
692
    }
693
}
694