Completed
Push — master ( c79300...9eb332 )
by Arne
02:48
created

Vault::getLocalIndexExclusionPatterns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
1
<?php
2
3
namespace Storeman;
4
5
use Storeman\Config\VaultConfiguration;
6
use Storeman\ConflictHandler\ConflictHandlerInterface;
7
use Storeman\Hash\HashContainer;
8
use Storeman\Index\Index;
9
use Storeman\Index\IndexObject;
10
use Storeman\Operation\WriteSynchronizationOperation;
11
use Storeman\StorageAdapter\StorageAdapterInterface;
12
use Storeman\IndexMerger\IndexMergerInterface;
13
use Storeman\LockAdapter\LockAdapterInterface;
14
use Storeman\OperationListBuilder\OperationListBuilderInterface;
15
use Storeman\SynchronizationProgressListener\DummySynchronizationProgressListener;
16
use Storeman\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
17
use Storeman\VaultLayout\VaultLayoutInterface;
18
use Storeman\Operation\OperationInterface;
19
20
class Vault
21
{
22
    public const LOCK_SYNC = 'sync';
23
24
25
    /**
26
     * @var Storeman
27
     */
28
    protected $storeman;
29
30
    /**
31
     * @var VaultConfiguration
32
     */
33
    protected $vaultConfiguration;
34
35
    /**
36
     * @var VaultLayoutInterface
37
     */
38
    protected $vaultLayout;
39
40
    /**
41
     * @var StorageAdapterInterface
42
     */
43
    protected $storageAdapter;
44
45
    /**
46
     * @var LockAdapterInterface
47
     */
48
    protected $lockAdapter;
49
50
    /**
51
     * @var IndexMergerInterface
52
     */
53
    protected $indexMerger;
54
55
    /**
56
     * @var ConflictHandlerInterface
57
     */
58
    protected $conflictHandler;
59
60
    /**
61
     * @var OperationListBuilderInterface
62
     */
63
    protected $operationListBuilder;
64
65
    public function __construct(Storeman $storeman, VaultConfiguration $vaultConfiguration)
66
    {
67
        $this->storeman = $storeman;
68
        $this->vaultConfiguration = $vaultConfiguration;
69
    }
70
71
    public function getStoreman(): Storeman
72
    {
73
        return $this->storeman;
74
    }
75
76
    public function getVaultConfiguration(): VaultConfiguration
77
    {
78
        return $this->vaultConfiguration;
79
    }
80
81
    public function getVaultLayout(): VaultLayoutInterface
82
    {
83
        return $this->vaultLayout ?: ($this->vaultLayout = $this->getContainer()->get('vaultLayout'));
84
    }
85
86
    public function getStorageAdapter(): StorageAdapterInterface
87
    {
88
        return $this->storageAdapter ?: ($this->storageAdapter = $this->getContainer()->get('storageAdapter'));
89
    }
90
91
    public function getLockAdapter(): LockAdapterInterface
92
    {
93
        return $this->lockAdapter ?: ($this->lockAdapter = $this->getContainer()->get('lockAdapter'));
94
    }
95
96
    public function getIndexMerger(): IndexMergerInterface
97
    {
98
        return $this->indexMerger ?: ($this->indexMerger = $this->getContainer()->get('indexMerger'));
99
    }
100
101
    public function getConflictHandler(): ConflictHandlerInterface
102
    {
103
        return $this->conflictHandler ?: ($this->conflictHandler = $this->getContainer()->get('conflictHandler'));
104
    }
105
106
    public function getOperationListBuilder(): OperationListBuilderInterface
107
    {
108
        return $this->operationListBuilder ?: ($this->operationListBuilder = $this->getContainer()->get('operationListBuilder'));
109
    }
110
111
    /**
112
     * Reads and returns the index representing the local state on the last synchronization.
113
     *
114
     * @return Index
115
     * @throws Exception
116
     */
117
    public function loadLastLocalIndex(): ?Index
118
    {
119
        $index = null;
120
        $path = $this->getLastLocalIndexFilePath();
121
122
        if (is_file($path))
123
        {
124
            $stream = fopen($path, 'rb');
125
126
            $index = new Index();
127
            while (($row = fgetcsv($stream)) !== false)
128
            {
129
                $index->addObject($this->createIndexObjectFromScalarArray($row));
130
            }
131
132
            fclose($stream);
133
        }
134
135
        return $index;
136
    }
137
138
    /**
139
     * Reads and returns the current remote index.
140
     *
141
     * @param int $revision Revision to load. Defaults to the last revision.
142
     *
143
     * @return Index
144
     */
145
    public function loadRemoteIndex(int $revision = null): ?Index
146
    {
147
        $synchronization = $revision ?
148
            $this->getVaultLayout()->getSynchronization($revision) :
149
            $this->getVaultLayout()->getLastSynchronization();
150
151
        return $synchronization ? $synchronization->getIndex() : null;
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->storeman->getLocalIndex();
174
        $lastLocalIndex = $this->loadLastLocalIndex();
175
        $remoteIndex = $this->loadRemoteIndex();
176
177
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
178
179
        return $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
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->storeman->getLocalIndex();
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 = $lastSynchronization->getIndex();
215
        }
216
        else
217
        {
218
            $newRevision = $newRevision ?: 1;
219
            $remoteIndex = null;
220
        }
221
222
        // compute merged index
223
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
224
225
        $synchronization = new Synchronization($newRevision, new \DateTime(), $this->storeman->getConfiguration()->getIdentity(), $mergedIndex);
226
227
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
228
        $operationList->addOperation(new WriteSynchronizationOperation($synchronization));
229
230
        $operationResultList = new OperationResultList();
231
232
        // operation count +
233
        // save merged index as last local index +
234
        // release lock
235
        $progressionListener->start(count($operationList) + 2);
236
237
        foreach ($operationList as $operation)
238
        {
239
            /** @var OperationInterface $operation */
240
241
            $success = $operation->execute($this->storeman->getConfiguration()->getPath(), $this->storeman->getFileReader(), $this->getVaultLayout());
242
243
            $operationResult = new OperationResult($operation, $success);
244
            $operationResultList->addOperationResult($operationResult);
245
246
            $progressionListener->advance();
247
        }
248
249
        // save merged index locally
250
        $this->writeLastLocalIndex($mergedIndex);
251
        $progressionListener->advance();
252
253
        // release lock
254
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
255
        {
256
            throw new Exception('Failed to release lock.');
257
        }
258
        $progressionListener->advance();
259
260
        $progressionListener->finish();
261
262
        return $operationResultList;
263
    }
264
265
    /**
266
     * Loads and returns the list of synchronizations from the vault.
267
     *
268
     * @return SynchronizationList
269
     */
270
    public function loadSynchronizationList(): SynchronizationList
271
    {
272
        return $this->getVaultLayout()->getSynchronizations();
273
    }
274
275
    /**
276
     * Restores the local state at the given revision from the vault.
277
     *
278
     * @param int $revision
279
     * @param SynchronizationProgressListenerInterface $progressionListener
280
     *
281
     * @return OperationResultList
282
     * @throws Exception
283
     */
284
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
285
    {
286
        return $this->doRestore($revision, $progressionListener);
287
    }
288
289
    /**
290
     * @param string $targetPath
291
     * @param int $revision
292
     * @param SynchronizationProgressListenerInterface|null $progressListener
293
     *
294
     * @return OperationResultList
295
     * @throws \Exception
296
     */
297
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
298
    {
299
        return $this->doRestore($revision, $progressListener, true, $targetPath);
300
    }
301
302
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null): Index
303
    {
304
        $localIndex = $localIndex ?: $this->storeman->getLocalIndex();
305
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
306
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
307
308
        if ($remoteIndex === null)
309
        {
310
            return $localIndex;
311
        }
312
313
        return $this->getIndexMerger()->merge($this->getConflictHandler(), $remoteIndex, $localIndex, $lastLocalIndex);
0 ignored issues
show
Bug introduced by
It seems like $lastLocalIndex defined by $lastLocalIndex ?: $this->loadLastLocalIndex() on line 305 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...
314
    }
315
316
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
317
    {
318
        if ($progressionListener === null)
319
        {
320
            $progressionListener = new DummySynchronizationProgressListener();
321
        }
322
323
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
324
        {
325
            throw new Exception('Failed to acquire lock.');
326
        }
327
328
        // fall back to last revision
329
        if ($revision === null)
330
        {
331
            $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
332
333
            if (!$lastSynchronization)
334
            {
335
                throw new Exception('No revision to restore from.');
336
            }
337
338
            $revision = $lastSynchronization->getRevision();
339
        }
340
341
        $remoteIndex = $this->loadRemoteIndex($revision);
342
343
        if ($remoteIndex === null)
344
        {
345
            throw new Exception("Unknown revision: {$revision}");
346
        }
347
348
        $targetPath = $targetPath ?: $this->storeman->getConfiguration()->getPath();
349
350
        $localIndex = $this->storeman->getLocalIndex($targetPath);
351
352
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex);
353
354
        $operationResultList = new OperationResultList();
355
356
        // operation count +
357
        // save merged index as last local index +
358
        // release lock
359
        $progressionListener->start(count($operationList) + 2);
360
361
        foreach ($operationList as $operation)
362
        {
363
            /** @var OperationInterface $operation */
364
365
            $success = $operation->execute($targetPath, $this->storeman->getFileReader(), $this->getVaultLayout());
366
367
            $operationResult = new OperationResult($operation, $success);
368
            $operationResultList->addOperationResult($operationResult);
369
370
            $progressionListener->advance();
371
        }
372
373
        if (!$skipLastLocalIndexUpdate)
374
        {
375
            $this->writeLastLocalIndex($remoteIndex);
376
        }
377
378
        $progressionListener->advance();
379
380
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
381
        {
382
            throw new Exception('Failed to release lock.');
383
        }
384
385
        $progressionListener->advance();
386
        $progressionListener->finish();
387
388
        return $operationResultList;
389
    }
390
391
    protected function writeLastLocalIndex(Index $index): void
392
    {
393
        $stream = fopen($this->getLastLocalIndexFilePath(), 'wb');
394
395
        foreach ($index as $object)
396
        {
397
            /** @var IndexObject $object */
398
399
            if (fputcsv($stream, $this->indexObjectToScalarArray($object)) === false)
400
            {
401
                throw new Exception("Writing to {$this->getLastLocalIndexFilePath()} failed");
402
            }
403
        }
404
405
        fclose($stream);
406
    }
407
408
    /**
409
     * Transforms an IndexObject instance into a scalar array suitable for fputcsv().
410
     *
411
     * @param IndexObject $indexObject
412
     * @return array
413
     */
414
    protected function indexObjectToScalarArray(IndexObject $indexObject): array
415
    {
416
        return [
417
            $indexObject->getRelativePath(),
418
            $indexObject->getType(),
419
            $indexObject->getMtime(),
420
            $indexObject->getCtime(),
421
            $indexObject->getPermissions(),
422
            $indexObject->getSize(),
423
            $indexObject->getInode(),
424
            $indexObject->getLinkTarget(),
425
            $indexObject->getBlobId(),
426
            $indexObject->getHashes() ? $indexObject->getHashes()->serialize() : null,
427
        ];
428
    }
429
430
    /**
431
     * Reconstructs an IndexObject instance from a scalar array read by fgetcsv().
432
     *
433
     * @param array $array
434
     * @return IndexObject
435
     */
436
    protected function createIndexObjectFromScalarArray(array $array): IndexObject
437
    {
438
        return new IndexObject(
439
            $array[0],
440
            (int)$array[1],
441
            (int)$array[2],
442
            (int)$array[3],
443
            (int)$array[4],
444
            ($array[5] !== '') ? (int)$array[5] : null,
445
            (int)$array[6],
446
            $array[7] ?: null,
447
            $array[8] ?: null,
448
            $array[9] ? (new HashContainer())->unserialize($array[9]) : null
449
        );
450
    }
451
452
    protected function getLastLocalIndexFilePath(): string
453
    {
454
        // todo: use other vault identifier
455
        return $this->storeman->getMetadataDirectoryPath() . sprintf('lastLocalIndex-%s', $this->vaultConfiguration->getTitle());
456
    }
457
458
    /**
459
     * Returns the service container with this vault as its context.
460
     *
461
     * @return Container
462
     */
463
    protected function getContainer(): Container
464
    {
465
        return $this->storeman->getContainer($this);
466
    }
467
}
468