Completed
Push — master ( b2e536...a4e6dd )
by Arne
02:05
created

Vault::readIndexFromStream()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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