Completed
Push — master ( a4e6dd...a851cc )
by Arne
02:45
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\ConflictHandler\ConflictHandlerInterface;
6
use Storeman\IndexBuilder\IndexBuilderInterface;
7
use Storeman\Operation\WriteSynchronizationOperation;
8
use Storeman\StorageAdapter\StorageAdapterInterface;
9
use Storeman\Exception\Exception;
10
use Storeman\IndexMerger\IndexMergerInterface;
11
use Storeman\LockAdapter\LockAdapterInterface;
12
use Storeman\OperationListBuilder\OperationListBuilderInterface;
13
use Storeman\SynchronizationProgressListener\DummySynchronizationProgressListener;
14
use Storeman\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
15
use Storeman\VaultLayout\VaultLayoutInterface;
16
use Storeman\Operation\OperationInterface;
17
18
class Vault
19
{
20
    public const CONFIG_FILE_NAME = 'storeman.json';
21
    public const METADATA_DIRECTORY_NAME = '.storeman';
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 IndexBuilderInterface
52
     */
53
    protected $indexBuilder;
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
    public function __construct(Storeman $storeman, VaultConfiguration $vaultConfiguration)
71
    {
72
        $this->storeman = $storeman;
73
        $this->vaultConfiguration = $vaultConfiguration;
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 getIndexBuilder(): IndexBuilderInterface
97
    {
98
        return $this->indexBuilder ?: ($this->indexBuilder = $this->getContainer()->get('indexBuilder'));
99
    }
100
101
    public function getIndexMerger(): IndexMergerInterface
102
    {
103
        return $this->indexMerger ?: ($this->indexMerger = $this->getContainer()->get('indexMerger'));
104
    }
105
106
    public function getConflictHandler(): ConflictHandlerInterface
107
    {
108
        return $this->conflictHandler ?: ($this->conflictHandler = $this->getContainer()->get('conflictHandler'));
109
    }
110
111
    public function getOperationListBuilder(): OperationListBuilderInterface
112
    {
113
        return $this->operationListBuilder ?: ($this->operationListBuilder = $this->getContainer()->get('operationListBuilder'));
114
    }
115
116
    /**
117
     * Builds and returns an index representing the current local state.
118
     *
119
     * @return Index
120
     */
121
    public function buildLocalIndex(): Index
122
    {
123
        return $this->getIndexBuilder()->buildIndexFromPath(
124
            $this->vaultConfiguration->getConfiguration()->getPath(),
125
            $this->getLocalIndexExclusionPatterns()
126
        );
127
    }
128
129
    /**
130
     * Reads and returns the index representing the local state on the last synchronization.
131
     *
132
     * @return Index
133
     * @throws Exception
134
     */
135
    public function loadLastLocalIndex(): ?Index
136
    {
137
        $index = null;
138
        $path = $this->getLastLocalIndexFilePath();
139
140
        if (is_file($path))
141
        {
142
            $stream = fopen($path, 'rb');
143
144
            $index = new Index();
145
            while (($row = fgetcsv($stream)) !== false)
146
            {
147
                $index->addObject(IndexObject::fromScalarArray($row));
148
            }
149
150
            fclose($stream);
151
        }
152
153
        return $index;
154
    }
155
156
    /**
157
     * Reads and returns the current remote index.
158
     *
159
     * @param int $revision Revision to load. Defaults to the last revision.
160
     *
161
     * @return Index
162
     */
163
    public function loadRemoteIndex(int $revision = null): ?Index
164
    {
165
        $synchronization = $revision ?
166
            $this->getVaultLayout()->getSynchronization($revision) :
167
            $this->getVaultLayout()->getLastSynchronization();
168
169
        return $synchronization ? $synchronization->getIndex() : null;
170
    }
171
172
    /**
173
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
174
     *
175
     * @return Index
176
     */
177
    public function buildMergedIndex(): Index
178
    {
179
        return $this->doBuildMergedIndex();
180
    }
181
182
    /**
183
     * Returns ordered list of operations required to synchronize the vault with the local path.
184
     * In addition to the object specific operations contained in the returned OperationList additional operations
185
     * might be necessary like index updates that do not belong to specific index objects.
186
     *
187
     * @return OperationList
188
     */
189
    public function getOperationList(): OperationList
190
    {
191
        $localIndex = $this->buildLocalIndex();
192
        $lastLocalIndex = $this->loadLastLocalIndex();
193
        $remoteIndex = $this->loadRemoteIndex();
194
195
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
196
197
        return $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
198
    }
199
200
    /**
201
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
202
     *
203
     * @param int $newRevision
204
     * @param SynchronizationProgressListenerInterface $progressionListener
205
     *
206
     * @return OperationResultList
207
     * @throws Exception
208
     */
209
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
210
    {
211
        if ($progressionListener === null)
212
        {
213
            $progressionListener = new DummySynchronizationProgressListener();
214
        }
215
216
        $localIndex = $this->buildLocalIndex();
217
        $lastLocalIndex = $this->loadLastLocalIndex();
218
219
220
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
221
        {
222
            throw new Exception('Failed to acquire lock.');
223
        }
224
225
226
        $synchronizationList = $this->loadSynchronizationList();
227
        $lastSynchronization = $synchronizationList->getLastSynchronization();
228
229
        if ($lastSynchronization)
230
        {
231
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
232
            $remoteIndex = $lastSynchronization->getIndex();
233
        }
234
        else
235
        {
236
            $newRevision = $newRevision ?: 1;
237
            $remoteIndex = null;
238
        }
239
240
        // compute merged index
241
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
242
243
        $synchronization = new Synchronization($newRevision, new \DateTime(), $this->storeman->getConfiguration()->getIdentity(), $mergedIndex);
244
245
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex, $remoteIndex);
246
        $operationList->addOperation(new WriteSynchronizationOperation($synchronization));
247
248
        $operationResultList = new OperationResultList();
249
250
        // operation count +
251
        // save merged index as last local index +
252
        // release lock
253
        $progressionListener->start(count($operationList) + 2);
254
255
        foreach ($operationList as $operation)
256
        {
257
            /** @var OperationInterface $operation */
258
259
            $success = $operation->execute($this->storeman->getConfiguration()->getPath(), $this->getVaultLayout());
260
261
            $operationResult = new OperationResult($operation, $success);
262
            $operationResultList->addOperationResult($operationResult);
263
264
            $progressionListener->advance();
265
        }
266
267
        // save merged index locally
268
        $this->writeLastLocalIndex($mergedIndex);
269
        $progressionListener->advance();
270
271
        // release lock
272
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
273
        {
274
            throw new Exception('Failed to release lock.');
275
        }
276
        $progressionListener->advance();
277
278
        $progressionListener->finish();
279
280
        return $operationResultList;
281
    }
282
283
    /**
284
     * Loads and returns the list of synchronizations from the vault.
285
     *
286
     * @return SynchronizationList
287
     */
288
    public function loadSynchronizationList(): SynchronizationList
289
    {
290
        return $this->getVaultLayout()->getSynchronizations();
291
    }
292
293
    /**
294
     * Restores the local state at the given revision from the vault.
295
     *
296
     * @param int $revision
297
     * @param SynchronizationProgressListenerInterface $progressionListener
298
     *
299
     * @return OperationResultList
300
     * @throws Exception
301
     */
302
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
303
    {
304
        return $this->doRestore($revision, $progressionListener);
305
    }
306
307
    /**
308
     * @param string $targetPath
309
     * @param int $revision
310
     * @param SynchronizationProgressListenerInterface|null $progressListener
311
     *
312
     * @return OperationResultList
313
     * @throws \Exception
314
     */
315
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
316
    {
317
        return $this->doRestore($revision, $progressListener, true, $targetPath);
318
    }
319
320
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null): Index
321
    {
322
        $localIndex = $localIndex ?: $this->buildLocalIndex();
323
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
324
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
325
326
        if ($remoteIndex === null)
327
        {
328
            return $localIndex;
329
        }
330
331
        return $this->getIndexMerger()->merge($this->getConflictHandler(), $remoteIndex, $localIndex, $lastLocalIndex);
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->loadRemoteIndex($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->getIndexBuilder()->buildIndexFromPath($targetPath, $this->getLocalIndexExclusionPatterns());
369
370
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex, $remoteIndex);
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->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
        $stream = fopen($this->getLastLocalIndexFilePath(), 'wb');
412
413
        foreach ($index as $object)
414
        {
415
            /** @var IndexObject $object */
416
417
            if (fputcsv($stream, $object->toScalarArray()) === false)
418
            {
419
                throw new Exception("Writing to {$this->getLastLocalIndexFilePath()} failed");
420
            }
421
        }
422
423
        fclose($stream);
424
    }
425
426
    protected function initMetadataDirectory(): string
427
    {
428
        $path = $this->storeman->getConfiguration()->getPath() . static::METADATA_DIRECTORY_NAME;
429
430
        if (!is_dir($path))
431
        {
432
            if (!mkdir($path))
433
            {
434
                throw new Exception("mkdir() failed for {$path}");
435
            }
436
        }
437
438
        return $path . DIRECTORY_SEPARATOR;
439
    }
440
441
    protected function getLastLocalIndexFilePath(): string
442
    {
443
        // todo: use other vault identifier
444
        return $this->initMetadataDirectory() . sprintf('lastLocalIndex-%s', $this->vaultConfiguration->getTitle());
445
    }
446
447
    /**
448
     * @return string[]
449
     */
450
    protected function getLocalIndexExclusionPatterns()
451
    {
452
        return array_merge($this->vaultConfiguration->getConfiguration()->getExclude(), [
453
            static::CONFIG_FILE_NAME,
454
            static::METADATA_DIRECTORY_NAME,
455
        ]);
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