Completed
Push — master ( e610ee...c79300 )
by Arne
04:09
created

Vault::indexObjectToScalarArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

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