Completed
Push — master ( 44a00f...9727c6 )
by Arne
02:20
created

Vault::doRestore()   B

Complexity

Conditions 9
Paths 40

Size

Total Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 52
rs 7.4917
c 0
b 0
f 0
cc 9
nc 40
nop 4

How to fix   Long Method   

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 Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerAwareTrait;
7
use Psr\Log\NullLogger;
8
use Storeman\Config\VaultConfiguration;
9
use Storeman\ConflictHandler\ConflictHandlerInterface;
10
use Storeman\Hash\HashContainer;
11
use Storeman\Index\Index;
12
use Storeman\Index\IndexObject;
13
use Storeman\Operation\WriteSynchronizationOperation;
14
use Storeman\StorageAdapter\StorageAdapterInterface;
15
use Storeman\IndexMerger\IndexMergerInterface;
16
use Storeman\LockAdapter\LockAdapterInterface;
17
use Storeman\OperationListBuilder\OperationListBuilderInterface;
18
use Storeman\SynchronizationProgressListener\DummySynchronizationProgressListener;
19
use Storeman\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
20
use Storeman\VaultLayout\VaultLayoutInterface;
21
22
class Vault implements LoggerAwareInterface
23
{
24
    use LoggerAwareTrait;
25
26
27
    public const LOCK_SYNC = 'sync';
28
29
30
    /**
31
     * @var Storeman
32
     */
33
    protected $storeman;
34
35
    /**
36
     * @var VaultConfiguration
37
     */
38
    protected $vaultConfiguration;
39
40
    /**
41
     * @var VaultLayoutInterface
42
     */
43
    protected $vaultLayout;
44
45
    /**
46
     * @var StorageAdapterInterface
47
     */
48
    protected $storageAdapter;
49
50
    /**
51
     * @var LockAdapterInterface
52
     */
53
    protected $lockAdapter;
54
55
    /**
56
     * @var IndexMergerInterface
57
     */
58
    protected $indexMerger;
59
60
    /**
61
     * @var ConflictHandlerInterface
62
     */
63
    protected $conflictHandler;
64
65
    /**
66
     * @var OperationListBuilderInterface
67
     */
68
    protected $operationListBuilder;
69
70
    /**
71
     * @var Index
72
     */
73
    protected $lastLocalIndex;
74
75
    public function __construct(Storeman $storeman, VaultConfiguration $vaultConfiguration)
76
    {
77
        $this->storeman = $storeman;
78
        $this->vaultConfiguration = $vaultConfiguration;
79
        $this->logger = new NullLogger();
80
    }
81
82
    public function getStoreman(): Storeman
83
    {
84
        return $this->storeman;
85
    }
86
87
    public function getVaultConfiguration(): VaultConfiguration
88
    {
89
        return $this->vaultConfiguration;
90
    }
91
92
    public function getVaultLayout(): VaultLayoutInterface
93
    {
94
        return $this->vaultLayout ?: ($this->vaultLayout = $this->getContainer()->get('vaultLayout'));
95
    }
96
97
    public function getStorageAdapter(): StorageAdapterInterface
98
    {
99
        return $this->storageAdapter ?: ($this->storageAdapter = $this->getContainer()->get('storageAdapter'));
100
    }
101
102
    public function getLockAdapter(): LockAdapterInterface
103
    {
104
        return $this->lockAdapter ?: ($this->lockAdapter = $this->getContainer()->get('lockAdapter'));
105
    }
106
107
    public function getIndexMerger(): IndexMergerInterface
108
    {
109
        return $this->indexMerger ?: ($this->indexMerger = $this->getContainer()->get('indexMerger'));
110
    }
111
112
    public function getConflictHandler(): ConflictHandlerInterface
113
    {
114
        return $this->conflictHandler ?: ($this->conflictHandler = $this->getContainer()->get('conflictHandler'));
115
    }
116
117
    public function getOperationListBuilder(): OperationListBuilderInterface
118
    {
119
        return $this->operationListBuilder ?: ($this->operationListBuilder = $this->getContainer()->get('operationListBuilder'));
120
    }
121
122
    /**
123
     * Reads and returns the index representing the local state on the last synchronization.
124
     *
125
     * @return Index
126
     * @throws Exception
127
     */
128
    public function getLastLocalIndex(): ?Index
129
    {
130
        if ($this->lastLocalIndex === null)
131
        {
132
            $index = null;
133
            $path = $this->getLastLocalIndexFilePath();
134
135
            if (is_file($path))
136
            {
137
                $this->logger->info("Reading in last local index from {$path}...");
138
139
                $stream = fopen($path, 'rb');
140
141
                $index = new Index();
142
                while (($row = fgetcsv($stream)) !== false)
143
                {
144
                    $index->addObject($this->createIndexObjectFromScalarArray($row));
145
                }
146
147
                fclose($stream);
148
149
                $this->logger->info("Read {$index->count()} records for last local index");
150
            }
151
            else
152
            {
153
                $this->logger->info("No last local index exists");
154
            }
155
156
            $this->lastLocalIndex = $index;
157
        }
158
159
        return $this->lastLocalIndex;
160
    }
161
162
    /**
163
     * Reads and returns the current remote index.
164
     *
165
     * @param int $revision Revision to load. Defaults to the last revision.
166
     *
167
     * @return Index
168
     */
169
    public function getRemoteIndex(int $revision = null): ?Index
170
    {
171
        $this->logger->info(sprintf("Loading %s remote index...", $revision ? "r{$revision}" : 'latest'));
172
173
        $synchronization = $revision ?
174
            $this->getVaultLayout()->getSynchronization($revision) :
175
            $this->getVaultLayout()->getLastSynchronization();
176
177
        return $synchronization ? $synchronization->getIndex() : null;
178
    }
179
180
    /**
181
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
182
     *
183
     * @return Index
184
     */
185
    public function getMergedIndex(): Index
186
    {
187
        return $this->doBuildMergedIndex();
188
    }
189
190
    /**
191
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
192
     *
193
     * @param int $newRevision
194
     * @param SynchronizationProgressListenerInterface $progressionListener
195
     *
196
     * @return OperationResultList
197
     * @throws Exception
198
     */
199
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
200
    {
201
        if ($progressionListener === null)
202
        {
203
            $progressionListener = new DummySynchronizationProgressListener();
204
        }
205
206
        $localIndex = $this->storeman->getLocalIndex();
207
        $lastLocalIndex = $this->getLastLocalIndex();
208
209
210
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
211
        {
212
            throw new Exception('Failed to acquire lock.');
213
        }
214
215
216
        $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
217
218
        if ($lastSynchronization)
219
        {
220
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
221
            $remoteIndex = $lastSynchronization->getIndex();
222
        }
223
        else
224
        {
225
            $newRevision = $newRevision ?: 1;
226
            $remoteIndex = null;
227
        }
228
229
        // compute merged index
230
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
231
232
        $synchronization = new Synchronization($newRevision, new \DateTime(), $this->storeman->getConfiguration()->getIdentity(), $mergedIndex);
233
234
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
235
        $operationList->add(new OperationListItem(new WriteSynchronizationOperation($synchronization)));
236
237
        $operationResultList = $this->executeOperationList($operationList, $this->storeman->getConfiguration()->getPath(), $progressionListener);
238
239
        // save merged index locally
240
        $this->writeLastLocalIndex($mergedIndex);
241
242
        // release lock
243
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
244
        {
245
            throw new Exception('Failed to release lock.');
246
        }
247
248
        return $operationResultList;
249
    }
250
251
    /**
252
     * Restores the local state at the given revision from the vault.
253
     *
254
     * @param int $revision
255
     * @param SynchronizationProgressListenerInterface $progressionListener
256
     *
257
     * @return OperationResultList
258
     * @throws Exception
259
     */
260
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
261
    {
262
        return $this->doRestore($revision, $progressionListener);
263
    }
264
265
    /**
266
     * @param string $targetPath
267
     * @param int $revision
268
     * @param SynchronizationProgressListenerInterface|null $progressListener
269
     *
270
     * @return OperationResultList
271
     * @throws \Exception
272
     */
273
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
274
    {
275
        return $this->doRestore($revision, $progressListener, true, $targetPath);
276
    }
277
278
    /**
279
     * Returns a hash that is the same for any vault referencing the same physical storage location.
280
     *
281
     * @return string
282
     */
283
    public function getHash(): string
284
    {
285
        return hash('sha1', implode([
286
            get_class($this->getStorageAdapter()),
287
            $this->getStorageAdapter()->getIdentificationString($this->vaultConfiguration),
288
        ]));
289
    }
290
291
    /**
292
     * Returns an identifier usable for UI.
293
     *
294
     * @return string
295
     */
296
    public function getIdentifier(): string
297
    {
298
        return "{$this->getHash()} ({$this->vaultConfiguration->getTitle()})";
299
    }
300
301
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null): Index
302
    {
303
        $localIndex = $localIndex ?: $this->storeman->getLocalIndex();
304
        $lastLocalIndex = $lastLocalIndex ?: $this->getLastLocalIndex();
305
        $remoteIndex = $remoteIndex ?: $this->getRemoteIndex();
306
307
        if ($remoteIndex === null)
308
        {
309
            return $localIndex;
310
        }
311
312
        return $this->getIndexMerger()->merge($this->getConflictHandler(), $remoteIndex, $localIndex, $lastLocalIndex, IndexMergerInterface::INJECT_BLOBID);
0 ignored issues
show
Bug introduced by
It seems like $lastLocalIndex defined by $lastLocalIndex ?: $this->getLastLocalIndex() on line 304 can be null; however, Storeman\IndexMerger\IndexMergerInterface::merge() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
313
    }
314
315
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
316
    {
317
        if ($progressionListener === null)
318
        {
319
            $progressionListener = new DummySynchronizationProgressListener();
320
        }
321
322
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
323
        {
324
            throw new Exception('Failed to acquire lock.');
325
        }
326
327
        // fall back to last revision
328
        if ($revision === null)
329
        {
330
            $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
331
332
            if (!$lastSynchronization)
333
            {
334
                throw new Exception('No revision to restore from.');
335
            }
336
337
            $revision = $lastSynchronization->getRevision();
338
        }
339
340
        $remoteIndex = $this->getRemoteIndex($revision);
341
342
        if ($remoteIndex === null)
343
        {
344
            throw new Exception("Unknown revision: {$revision}");
345
        }
346
347
        $targetPath = $targetPath ?: $this->storeman->getConfiguration()->getPath();
348
349
        $localIndex = $this->storeman->getLocalIndex($targetPath);
350
351
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex);
352
353
        $operationResultList = $this->executeOperationList($operationList, $targetPath, $progressionListener);
354
355
        if (!$skipLastLocalIndexUpdate)
356
        {
357
            $this->writeLastLocalIndex($remoteIndex);
358
        }
359
360
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
361
        {
362
            throw new Exception('Failed to release lock.');
363
        }
364
365
        return $operationResultList;
366
    }
367
368
    protected function executeOperationList(OperationList $operationList, string $basePath, SynchronizationProgressListenerInterface $progressionListener): OperationResultList
369
    {
370
        $this->logger->notice(sprintf("Executing %d operation(s)...", count($operationList)));
371
372
        $operationResultList = new OperationResultList();
373
374
        $progressionListener->start(count($operationList));
375
        foreach ($operationList as $index => $operationListItem)
376
        {
377
            /** @var OperationListItem $operationListItem */
378
379
            $this->logger->debug(sprintf("#%d {$operationListItem->getOperation()}", $index + 1));
380
381
            $success = $operationListItem->getOperation()->execute($basePath, $this->storeman->getFileReader(), $this->getVaultLayout());
382
383
            if ($success && $indexObject = $operationListItem->getIndexObject())
384
            {
385
                $absolutePath = PathUtils::getAbsolutePath($basePath . $indexObject->getRelativePath());
386
387
                if (is_file($absolutePath))
388
                {
389
                    $indexObject->setCtime(FilesystemUtility::lstat($absolutePath)['ctime']);
390
                }
391
            }
392
393
            $operationResult = new OperationResult($operationListItem->getOperation(), $success);
394
            $operationResultList->addOperationResult($operationResult);
395
396
            $progressionListener->advance();
397
        }
398
        $progressionListener->finish();
399
400
        return $operationResultList;
401
    }
402
403
    protected function writeLastLocalIndex(Index $index): void
404
    {
405
        $this->logger->info(sprintf("Writing last local index with %d records to %s", $index->count(), $this->getLastLocalIndexFilePath()));
406
407
        // prevent outdated cache on failure
408
        $this->lastLocalIndex = null;
409
410
        $stream = fopen($this->getLastLocalIndexFilePath(), 'wb');
411
412
        foreach ($index as $object)
413
        {
414
            /** @var IndexObject $object */
415
416
            if (fputcsv($stream, $this->indexObjectToScalarArray($object)) === false)
417
            {
418
                throw new Exception("Writing to {$this->getLastLocalIndexFilePath()} failed");
419
            }
420
        }
421
422
        fclose($stream);
423
424
        // update local cache
425
        $this->lastLocalIndex = $index;
426
    }
427
428
    /**
429
     * Transforms an IndexObject instance into a scalar array suitable for fputcsv().
430
     *
431
     * @param IndexObject $indexObject
432
     * @return array
433
     */
434
    protected function indexObjectToScalarArray(IndexObject $indexObject): array
435
    {
436
        return [
437
            $indexObject->getRelativePath(),
438
            $indexObject->getType(),
439
            sprintf('%.9f', $indexObject->getMtime()),
440
            sprintf('%.9f', $indexObject->getCtime()),
441
            $indexObject->getPermissions(),
442
            $indexObject->getSize(),
443
            $indexObject->getInode(),
444
            $indexObject->getLinkTarget(),
445
            $indexObject->getBlobId(),
446
            $indexObject->getHashes() ? $indexObject->getHashes()->serialize() : null,
447
        ];
448
    }
449
450
    /**
451
     * Reconstructs an IndexObject instance from a scalar array read by fgetcsv().
452
     *
453
     * @param array $array
454
     * @return IndexObject
455
     */
456
    protected function createIndexObjectFromScalarArray(array $array): IndexObject
457
    {
458
        return new IndexObject(
459
            $array[0],
460
            (int)$array[1],
461
            (float)$array[2],
462
            (float)$array[3],
463
            (int)$array[4],
464
            ($array[5] !== '') ? (int)$array[5] : null,
465
            (int)$array[6],
466
            $array[7] ?: null,
467
            $array[8] ?: null,
468
            $array[9] ? (new HashContainer())->unserialize($array[9]) : null
469
        );
470
    }
471
472
    protected function getLastLocalIndexFilePath(): string
473
    {
474
        return $this->storeman->getMetadataDirectoryPath() . sprintf('lastLocalIndex-%s', $this->getHash());
475
    }
476
477
    /**
478
     * Returns the service container with this vault as its context.
479
     *
480
     * @return Container
481
     */
482
    protected function getContainer(): Container
483
    {
484
        return $this->storeman->getContainer($this);
485
    }
486
}
487