Completed
Push — master ( a8c5d7...b64b96 )
by Arne
02:19
created

Vault::getIdentifier()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
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
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
193
     *
194
     * @param int $newRevision
195
     * @param SynchronizationProgressListenerInterface $progressionListener
196
     *
197
     * @return OperationResultList
198
     * @throws Exception
199
     */
200
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
201
    {
202
        if ($progressionListener === null)
203
        {
204
            $progressionListener = new DummySynchronizationProgressListener();
205
        }
206
207
        $localIndex = $this->storeman->getLocalIndex();
208
        $lastLocalIndex = $this->getLastLocalIndex();
209
210
211
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
212
        {
213
            throw new Exception('Failed to acquire lock.');
214
        }
215
216
217
        $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
218
219
        if ($lastSynchronization)
220
        {
221
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
222
            $remoteIndex = $lastSynchronization->getIndex();
223
        }
224
        else
225
        {
226
            $newRevision = $newRevision ?: 1;
227
            $remoteIndex = null;
228
        }
229
230
        // compute merged index
231
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
232
233
        $synchronization = new Synchronization($newRevision, new \DateTime(), $this->storeman->getConfiguration()->getIdentity(), $mergedIndex);
234
235
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
236
        $operationList->addOperation(new WriteSynchronizationOperation($synchronization));
237
238
        $operationResultList = new OperationResultList();
239
240
        // operation count +
241
        // save merged index as last local index +
242
        // release lock
243
        $progressionListener->start(count($operationList) + 2);
244
245
        foreach ($operationList as $operation)
246
        {
247
            /** @var OperationInterface $operation */
248
249
            $success = $operation->execute($this->storeman->getConfiguration()->getPath(), $this->storeman->getFileReader(), $this->getVaultLayout());
250
251
            $operationResult = new OperationResult($operation, $success);
252
            $operationResultList->addOperationResult($operationResult);
253
254
            $progressionListener->advance();
255
        }
256
257
        // save merged index locally
258
        $this->writeLastLocalIndex($mergedIndex);
259
        $progressionListener->advance();
260
261
        // release lock
262
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
263
        {
264
            throw new Exception('Failed to release lock.');
265
        }
266
        $progressionListener->advance();
267
268
        $progressionListener->finish();
269
270
        return $operationResultList;
271
    }
272
273
    /**
274
     * Restores the local state at the given revision from the vault.
275
     *
276
     * @param int $revision
277
     * @param SynchronizationProgressListenerInterface $progressionListener
278
     *
279
     * @return OperationResultList
280
     * @throws Exception
281
     */
282
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
283
    {
284
        return $this->doRestore($revision, $progressionListener);
285
    }
286
287
    /**
288
     * @param string $targetPath
289
     * @param int $revision
290
     * @param SynchronizationProgressListenerInterface|null $progressListener
291
     *
292
     * @return OperationResultList
293
     * @throws \Exception
294
     */
295
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
296
    {
297
        return $this->doRestore($revision, $progressListener, true, $targetPath);
298
    }
299
300
    /**
301
     * Returns a hash that is the same for any vault referencing the same physical storage location.
302
     *
303
     * @return string
304
     */
305
    public function getHash(): string
306
    {
307
        return hash('sha1', $this->getStorageAdapter()->getIdentificationString($this->vaultConfiguration));
308
    }
309
310
    /**
311
     * Returns an identifier usable for UI.
312
     *
313
     * @return string
314
     */
315
    public function getIdentifier(): string
316
    {
317
        return "{$this->getHash()} ({$this->vaultConfiguration->getTitle()})";
318
    }
319
320
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null): Index
321
    {
322
        $localIndex = $localIndex ?: $this->storeman->getLocalIndex();
323
        $lastLocalIndex = $lastLocalIndex ?: $this->getLastLocalIndex();
324
        $remoteIndex = $remoteIndex ?: $this->getRemoteIndex();
325
326
        if ($remoteIndex === null)
327
        {
328
            return $localIndex;
329
        }
330
331
        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 323 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...
332
    }
333
334
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
335
    {
336
        if ($progressionListener === null)
337
        {
338
            $progressionListener = new DummySynchronizationProgressListener();
339
        }
340
341
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
342
        {
343
            throw new Exception('Failed to acquire lock.');
344
        }
345
346
        // fall back to last revision
347
        if ($revision === null)
348
        {
349
            $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
350
351
            if (!$lastSynchronization)
352
            {
353
                throw new Exception('No revision to restore from.');
354
            }
355
356
            $revision = $lastSynchronization->getRevision();
357
        }
358
359
        $remoteIndex = $this->getRemoteIndex($revision);
360
361
        if ($remoteIndex === null)
362
        {
363
            throw new Exception("Unknown revision: {$revision}");
364
        }
365
366
        $targetPath = $targetPath ?: $this->storeman->getConfiguration()->getPath();
367
368
        $localIndex = $this->storeman->getLocalIndex($targetPath);
369
370
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex);
371
372
        $operationResultList = new OperationResultList();
373
374
        // operation count +
375
        // save merged index as last local index +
376
        // release lock
377
        $progressionListener->start(count($operationList) + 2);
378
379
        foreach ($operationList as $operation)
380
        {
381
            /** @var OperationInterface $operation */
382
383
            $success = $operation->execute($targetPath, $this->storeman->getFileReader(), $this->getVaultLayout());
384
385
            $operationResult = new OperationResult($operation, $success);
386
            $operationResultList->addOperationResult($operationResult);
387
388
            $progressionListener->advance();
389
        }
390
391
        if (!$skipLastLocalIndexUpdate)
392
        {
393
            $this->writeLastLocalIndex($remoteIndex);
394
        }
395
396
        $progressionListener->advance();
397
398
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
399
        {
400
            throw new Exception('Failed to release lock.');
401
        }
402
403
        $progressionListener->advance();
404
        $progressionListener->finish();
405
406
        return $operationResultList;
407
    }
408
409
    protected function writeLastLocalIndex(Index $index): void
410
    {
411
        $this->logger->info(sprintf("Writing last local index with %d records to %s", $index->count(), $this->getLastLocalIndexFilePath()));
412
413
        // prevent outdated cache on failure
414
        $this->lastLocalIndex = null;
415
416
        $stream = fopen($this->getLastLocalIndexFilePath(), 'wb');
417
418
        foreach ($index as $object)
419
        {
420
            /** @var IndexObject $object */
421
422
            if (fputcsv($stream, $this->indexObjectToScalarArray($object)) === false)
423
            {
424
                throw new Exception("Writing to {$this->getLastLocalIndexFilePath()} failed");
425
            }
426
        }
427
428
        fclose($stream);
429
430
        // update local cache
431
        $this->lastLocalIndex = $index;
432
    }
433
434
    /**
435
     * Transforms an IndexObject instance into a scalar array suitable for fputcsv().
436
     *
437
     * @param IndexObject $indexObject
438
     * @return array
439
     */
440
    protected function indexObjectToScalarArray(IndexObject $indexObject): array
441
    {
442
        return [
443
            $indexObject->getRelativePath(),
444
            $indexObject->getType(),
445
            $indexObject->getMtime(),
446
            $indexObject->getCtime(),
447
            $indexObject->getPermissions(),
448
            $indexObject->getSize(),
449
            $indexObject->getInode(),
450
            $indexObject->getLinkTarget(),
451
            $indexObject->getBlobId(),
452
            $indexObject->getHashes() ? $indexObject->getHashes()->serialize() : null,
453
        ];
454
    }
455
456
    /**
457
     * Reconstructs an IndexObject instance from a scalar array read by fgetcsv().
458
     *
459
     * @param array $array
460
     * @return IndexObject
461
     */
462
    protected function createIndexObjectFromScalarArray(array $array): IndexObject
463
    {
464
        return new IndexObject(
465
            $array[0],
466
            (int)$array[1],
467
            (int)$array[2],
468
            (int)$array[3],
469
            (int)$array[4],
470
            ($array[5] !== '') ? (int)$array[5] : null,
471
            (int)$array[6],
472
            $array[7] ?: null,
473
            $array[8] ?: null,
474
            $array[9] ? (new HashContainer())->unserialize($array[9]) : null
475
        );
476
    }
477
478
    protected function getLastLocalIndexFilePath(): string
479
    {
480
        return $this->storeman->getMetadataDirectoryPath() . sprintf('lastLocalIndex-%s', $this->getHash());
481
    }
482
483
    /**
484
     * Returns the service container with this vault as its context.
485
     *
486
     * @return Container
487
     */
488
    protected function getContainer(): Container
489
    {
490
        return $this->storeman->getContainer($this);
491
    }
492
}
493