Completed
Push — master ( 038c42...a8c5d7 )
by Arne
03:50
created

Vault::getRemoteIndex()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
cc 4
nc 4
nop 1
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
use Storeman\Operation\OperationInterface;
22
23
class Vault implements LoggerAwareInterface
24
{
25
    use LoggerAwareTrait;
26
27
28
    public const LOCK_SYNC = 'sync';
29
30
31
    /**
32
     * @var Storeman
33
     */
34
    protected $storeman;
35
36
    /**
37
     * @var VaultConfiguration
38
     */
39
    protected $vaultConfiguration;
40
41
    /**
42
     * @var VaultLayoutInterface
43
     */
44
    protected $vaultLayout;
45
46
    /**
47
     * @var StorageAdapterInterface
48
     */
49
    protected $storageAdapter;
50
51
    /**
52
     * @var LockAdapterInterface
53
     */
54
    protected $lockAdapter;
55
56
    /**
57
     * @var IndexMergerInterface
58
     */
59
    protected $indexMerger;
60
61
    /**
62
     * @var ConflictHandlerInterface
63
     */
64
    protected $conflictHandler;
65
66
    /**
67
     * @var OperationListBuilderInterface
68
     */
69
    protected $operationListBuilder;
70
71
    /**
72
     * @var Index
73
     */
74
    protected $lastLocalIndex;
75
76
    public function __construct(Storeman $storeman, VaultConfiguration $vaultConfiguration)
77
    {
78
        $this->storeman = $storeman;
79
        $this->vaultConfiguration = $vaultConfiguration;
80
        $this->logger = new NullLogger();
81
    }
82
83
    public function getStoreman(): Storeman
84
    {
85
        return $this->storeman;
86
    }
87
88
    public function getVaultConfiguration(): VaultConfiguration
89
    {
90
        return $this->vaultConfiguration;
91
    }
92
93
    public function getVaultLayout(): VaultLayoutInterface
94
    {
95
        return $this->vaultLayout ?: ($this->vaultLayout = $this->getContainer()->get('vaultLayout'));
96
    }
97
98
    public function getStorageAdapter(): StorageAdapterInterface
99
    {
100
        return $this->storageAdapter ?: ($this->storageAdapter = $this->getContainer()->get('storageAdapter'));
101
    }
102
103
    public function getLockAdapter(): LockAdapterInterface
104
    {
105
        return $this->lockAdapter ?: ($this->lockAdapter = $this->getContainer()->get('lockAdapter'));
106
    }
107
108
    public function getIndexMerger(): IndexMergerInterface
109
    {
110
        return $this->indexMerger ?: ($this->indexMerger = $this->getContainer()->get('indexMerger'));
111
    }
112
113
    public function getConflictHandler(): ConflictHandlerInterface
114
    {
115
        return $this->conflictHandler ?: ($this->conflictHandler = $this->getContainer()->get('conflictHandler'));
116
    }
117
118
    public function getOperationListBuilder(): OperationListBuilderInterface
119
    {
120
        return $this->operationListBuilder ?: ($this->operationListBuilder = $this->getContainer()->get('operationListBuilder'));
121
    }
122
123
    /**
124
     * Reads and returns the index representing the local state on the last synchronization.
125
     *
126
     * @return Index
127
     * @throws Exception
128
     */
129
    public function getLastLocalIndex(): ?Index
130
    {
131
        if ($this->lastLocalIndex === null)
132
        {
133
            $index = null;
134
            $path = $this->getLastLocalIndexFilePath();
135
136
            if (is_file($path))
137
            {
138
                $this->logger->info("Reading in last local index from {$path}...");
139
140
                $stream = fopen($path, 'rb');
141
142
                $index = new Index();
143
                while (($row = fgetcsv($stream)) !== false)
144
                {
145
                    $index->addObject($this->createIndexObjectFromScalarArray($row));
146
                }
147
148
                fclose($stream);
149
150
                $this->logger->info("Read {$index->count()} records for last local index");
151
            }
152
            else
153
            {
154
                $this->logger->info("No last local index exists");
155
            }
156
157
            $this->lastLocalIndex = $index;
158
        }
159
160
        return $this->lastLocalIndex;
161
    }
162
163
    /**
164
     * Reads and returns the current remote index.
165
     *
166
     * @param int $revision Revision to load. Defaults to the last revision.
167
     *
168
     * @return Index
169
     */
170
    public function getRemoteIndex(int $revision = null): ?Index
171
    {
172
        $this->logger->info(sprintf("Loading %s remote index...", $revision ? "r{$revision}" : 'latest'));
173
174
        $synchronization = $revision ?
175
            $this->getVaultLayout()->getSynchronization($revision) :
176
            $this->getVaultLayout()->getLastSynchronization();
177
178
        return $synchronization ? $synchronization->getIndex() : null;
179
    }
180
181
    /**
182
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
183
     *
184
     * @return Index
185
     */
186
    public function getMergedIndex(): Index
187
    {
188
        return $this->doBuildMergedIndex();
189
    }
190
191
    /**
192
     * Returns ordered list of operations required to synchronize the vault with the local path.
193
     * In addition to the object specific operations contained in the returned OperationList additional operations
194
     * might be necessary like index updates that do not belong to specific index objects.
195
     *
196
     * @return OperationList
197
     */
198
    public function getOperationList(): OperationList
199
    {
200
        $localIndex = $this->storeman->getLocalIndex();
201
        $lastLocalIndex = $this->getLastLocalIndex();
202
        $remoteIndex = $this->getRemoteIndex();
203
204
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
205
206
        return $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
207
    }
208
209
    /**
210
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
211
     *
212
     * @param int $newRevision
213
     * @param SynchronizationProgressListenerInterface $progressionListener
214
     *
215
     * @return OperationResultList
216
     * @throws Exception
217
     */
218
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
219
    {
220
        if ($progressionListener === null)
221
        {
222
            $progressionListener = new DummySynchronizationProgressListener();
223
        }
224
225
        $localIndex = $this->storeman->getLocalIndex();
226
        $lastLocalIndex = $this->getLastLocalIndex();
227
228
229
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
230
        {
231
            throw new Exception('Failed to acquire lock.');
232
        }
233
234
235
        $synchronizationList = $this->loadSynchronizationList();
236
        $lastSynchronization = $synchronizationList->getLastSynchronization();
237
238
        if ($lastSynchronization)
239
        {
240
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
241
            $remoteIndex = $lastSynchronization->getIndex();
242
        }
243
        else
244
        {
245
            $newRevision = $newRevision ?: 1;
246
            $remoteIndex = null;
247
        }
248
249
        // compute merged index
250
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
251
252
        $synchronization = new Synchronization($newRevision, new \DateTime(), $this->storeman->getConfiguration()->getIdentity(), $mergedIndex);
253
254
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
255
        $operationList->addOperation(new WriteSynchronizationOperation($synchronization));
256
257
        $operationResultList = new OperationResultList();
258
259
        // operation count +
260
        // save merged index as last local index +
261
        // release lock
262
        $progressionListener->start(count($operationList) + 2);
263
264
        foreach ($operationList as $operation)
265
        {
266
            /** @var OperationInterface $operation */
267
268
            $success = $operation->execute($this->storeman->getConfiguration()->getPath(), $this->storeman->getFileReader(), $this->getVaultLayout());
269
270
            $operationResult = new OperationResult($operation, $success);
271
            $operationResultList->addOperationResult($operationResult);
272
273
            $progressionListener->advance();
274
        }
275
276
        // save merged index locally
277
        $this->writeLastLocalIndex($mergedIndex);
278
        $progressionListener->advance();
279
280
        // release lock
281
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
282
        {
283
            throw new Exception('Failed to release lock.');
284
        }
285
        $progressionListener->advance();
286
287
        $progressionListener->finish();
288
289
        return $operationResultList;
290
    }
291
292
    /**
293
     * Loads and returns the list of synchronizations from the vault.
294
     *
295
     * @return SynchronizationList
296
     */
297
    public function loadSynchronizationList(): SynchronizationList
298
    {
299
        return $this->getVaultLayout()->getSynchronizations();
300
    }
301
302
    /**
303
     * Restores the local state at the given revision from the vault.
304
     *
305
     * @param int $revision
306
     * @param SynchronizationProgressListenerInterface $progressionListener
307
     *
308
     * @return OperationResultList
309
     * @throws Exception
310
     */
311
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
312
    {
313
        return $this->doRestore($revision, $progressionListener);
314
    }
315
316
    /**
317
     * @param string $targetPath
318
     * @param int $revision
319
     * @param SynchronizationProgressListenerInterface|null $progressListener
320
     *
321
     * @return OperationResultList
322
     * @throws \Exception
323
     */
324
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
325
    {
326
        return $this->doRestore($revision, $progressListener, true, $targetPath);
327
    }
328
329
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null): Index
330
    {
331
        $localIndex = $localIndex ?: $this->storeman->getLocalIndex();
332
        $lastLocalIndex = $lastLocalIndex ?: $this->getLastLocalIndex();
333
        $remoteIndex = $remoteIndex ?: $this->getRemoteIndex();
334
335
        if ($remoteIndex === null)
336
        {
337
            return $localIndex;
338
        }
339
340
        return $this->getIndexMerger()->merge($this->getConflictHandler(), $remoteIndex, $localIndex, $lastLocalIndex);
0 ignored issues
show
Bug introduced by
It seems like $lastLocalIndex defined by $lastLocalIndex ?: $this->getLastLocalIndex() on line 332 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...
341
    }
342
343
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
344
    {
345
        if ($progressionListener === null)
346
        {
347
            $progressionListener = new DummySynchronizationProgressListener();
348
        }
349
350
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
351
        {
352
            throw new Exception('Failed to acquire lock.');
353
        }
354
355
        // fall back to last revision
356
        if ($revision === null)
357
        {
358
            $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
359
360
            if (!$lastSynchronization)
361
            {
362
                throw new Exception('No revision to restore from.');
363
            }
364
365
            $revision = $lastSynchronization->getRevision();
366
        }
367
368
        $remoteIndex = $this->getRemoteIndex($revision);
369
370
        if ($remoteIndex === null)
371
        {
372
            throw new Exception("Unknown revision: {$revision}");
373
        }
374
375
        $targetPath = $targetPath ?: $this->storeman->getConfiguration()->getPath();
376
377
        $localIndex = $this->storeman->getLocalIndex($targetPath);
378
379
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex);
380
381
        $operationResultList = new OperationResultList();
382
383
        // operation count +
384
        // save merged index as last local index +
385
        // release lock
386
        $progressionListener->start(count($operationList) + 2);
387
388
        foreach ($operationList as $operation)
389
        {
390
            /** @var OperationInterface $operation */
391
392
            $success = $operation->execute($targetPath, $this->storeman->getFileReader(), $this->getVaultLayout());
393
394
            $operationResult = new OperationResult($operation, $success);
395
            $operationResultList->addOperationResult($operationResult);
396
397
            $progressionListener->advance();
398
        }
399
400
        if (!$skipLastLocalIndexUpdate)
401
        {
402
            $this->writeLastLocalIndex($remoteIndex);
403
        }
404
405
        $progressionListener->advance();
406
407
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
408
        {
409
            throw new Exception('Failed to release lock.');
410
        }
411
412
        $progressionListener->advance();
413
        $progressionListener->finish();
414
415
        return $operationResultList;
416
    }
417
418
    protected function writeLastLocalIndex(Index $index): void
419
    {
420
        $this->logger->info(sprintf("Writing last local index with %d records to %s", $index->count(), $this->getLastLocalIndexFilePath()));
421
422
        // prevent outdated cache on failure
423
        $this->lastLocalIndex = null;
424
425
        $stream = fopen($this->getLastLocalIndexFilePath(), 'wb');
426
427
        foreach ($index as $object)
428
        {
429
            /** @var IndexObject $object */
430
431
            if (fputcsv($stream, $this->indexObjectToScalarArray($object)) === false)
432
            {
433
                throw new Exception("Writing to {$this->getLastLocalIndexFilePath()} failed");
434
            }
435
        }
436
437
        fclose($stream);
438
439
        // update local cache
440
        $this->lastLocalIndex = $index;
441
    }
442
443
    /**
444
     * Transforms an IndexObject instance into a scalar array suitable for fputcsv().
445
     *
446
     * @param IndexObject $indexObject
447
     * @return array
448
     */
449
    protected function indexObjectToScalarArray(IndexObject $indexObject): array
450
    {
451
        return [
452
            $indexObject->getRelativePath(),
453
            $indexObject->getType(),
454
            $indexObject->getMtime(),
455
            $indexObject->getCtime(),
456
            $indexObject->getPermissions(),
457
            $indexObject->getSize(),
458
            $indexObject->getInode(),
459
            $indexObject->getLinkTarget(),
460
            $indexObject->getBlobId(),
461
            $indexObject->getHashes() ? $indexObject->getHashes()->serialize() : null,
462
        ];
463
    }
464
465
    /**
466
     * Reconstructs an IndexObject instance from a scalar array read by fgetcsv().
467
     *
468
     * @param array $array
469
     * @return IndexObject
470
     */
471
    protected function createIndexObjectFromScalarArray(array $array): IndexObject
472
    {
473
        return new IndexObject(
474
            $array[0],
475
            (int)$array[1],
476
            (int)$array[2],
477
            (int)$array[3],
478
            (int)$array[4],
479
            ($array[5] !== '') ? (int)$array[5] : null,
480
            (int)$array[6],
481
            $array[7] ?: null,
482
            $array[8] ?: null,
483
            $array[9] ? (new HashContainer())->unserialize($array[9]) : null
484
        );
485
    }
486
487
    protected function getLastLocalIndexFilePath(): string
488
    {
489
        // todo: use other vault identifier
490
        return $this->storeman->getMetadataDirectoryPath() . sprintf('lastLocalIndex-%s', $this->vaultConfiguration->getTitle());
491
    }
492
493
    /**
494
     * Returns the service container with this vault as its context.
495
     *
496
     * @return Container
497
     */
498
    protected function getContainer(): Container
499
    {
500
        return $this->storeman->getContainer($this);
501
    }
502
}
503