Completed
Push — master ( 2cde75...6c52d6 )
by Arne
02:04
created

Vault::doRestore()   C

Complexity

Conditions 10
Paths 72

Size

Total Lines 73
Code Lines 31

Duplication

Lines 22
Ratio 30.14 %

Importance

Changes 0
Metric Value
dl 22
loc 73
rs 5.8564
c 0
b 0
f 0
cc 10
eloc 31
nc 72
nop 4

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Vault::readIndexFromStream() 0 16 3
A Vault::writeIndexToFile() 0 16 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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