Completed
Push — master ( 63b887...b350e3 )
by Arne
01:47
created

Vault::doBuildMergedIndex()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 7
nc 16
nop 3
1
<?php
2
3
namespace Archivr;
4
5
use Archivr\ConnectionAdapter\ConnectionAdapterInterface;
6
use Archivr\Exception\Exception;
7
use Archivr\IndexMerger\IndexMergerInterface;
8
use Archivr\IndexMerger\StandardIndexMerger;
9
use Archivr\LockAdapter\ConnectionBasedLockAdapter;
10
use Archivr\LockAdapter\LockAdapterInterface;
11
use Archivr\SynchronizationProgressListener\DummySynchronizationProgressListener;
12
use Archivr\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
13
use Ramsey\Uuid\Uuid;
14
use Symfony\Component\Finder\Finder;
15
use Symfony\Component\Finder\SplFileInfo;
16
use Archivr\Operation\ChmodOperation;
17
use Archivr\Operation\DownloadOperation;
18
use Archivr\Operation\MkdirOperation;
19
use Archivr\Operation\OperationInterface;
20
use Archivr\Operation\SymlinkOperation;
21
use Archivr\Operation\TouchOperation;
22
use Archivr\Operation\UnlinkOperation;
23
use Archivr\Operation\UploadOperation;
24
25
class Vault
26
{
27
    use TildeExpansionTrait;
28
29
    const METADATA_DIRECTORY_NAME = '.archivr';
30
    const SYNCHRONIZATION_LIST_FILE_NAME = 'index';
31
    const LOCK_SYNC = 'sync';
32
33
    // Operations modes for building operation collection
34
    const MODE_PREFER_LOCAL = 'local';
35
    const MODE_PREFER_REMOTE = 'remote';
36
37
    /**
38
     * @var string
39
     */
40
    protected $title;
41
42
    /**
43
     * @var ConnectionAdapterInterface
44
     */
45
    protected $vaultConnection;
46
47
    /**
48
     * @var string
49
     */
50
    protected $localPath;
51
52
    /**
53
     * @var LockAdapterInterface
54
     */
55
    protected $lockAdapter;
56
57
    /**
58
     * @var IndexMergerInterface
59
     */
60
    protected $indexMerger;
61
62
    /**
63
     * @var string[]
64
     */
65
    protected $exclusions = [];
66
67
    /**
68
     * @var string
69
     */
70
    protected $identity;
71
72
73
    public function __construct(string $title, string $localPath, ConnectionAdapterInterface $vaultConnection)
74
    {
75
        $this->title = $title;
76
        $this->vaultConnection = $vaultConnection;
77
        $this->localPath = rtrim($this->expandTildePath($localPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
78
    }
79
80
    public function getTitle(): string
81
    {
82
        return $this->title;
83
    }
84
85
    public function getLocalPath(): string
86
    {
87
        return $this->localPath;
88
    }
89
90
    public function setIndexMerger(IndexMergerInterface $indexMerger = null)
91
    {
92
        $this->indexMerger = $indexMerger;
93
94
        return $this;
95
    }
96
97
    public function getIndexMerger(): IndexMergerInterface
98
    {
99
        if ($this->indexMerger === null)
100
        {
101
            $this->indexMerger = new StandardIndexMerger();
102
        }
103
104
        return $this->indexMerger;
105
    }
106
107
    public function setLockAdapter(LockAdapterInterface $lockAdapter = null): Vault
108
    {
109
        $this->lockAdapter = $lockAdapter;
110
111
        return $this;
112
    }
113
114
    public function getLockAdapter(): LockAdapterInterface
115
    {
116
        if ($this->lockAdapter === null)
117
        {
118
            $this->lockAdapter = new ConnectionBasedLockAdapter($this->vaultConnection);
119
        }
120
121
        return $this->lockAdapter;
122
    }
123
124
    public function getVaultConnection(): ConnectionAdapterInterface
125
    {
126
        return $this->vaultConnection;
127
    }
128
129
    public function getExclusions(): array
130
    {
131
        return $this->exclusions;
132
    }
133
134
    public function addExclusion(string $path): Vault
135
    {
136
        $this->exclusions[] = $path;
137
138
        return $this;
139
    }
140
141
    public function setExclusions(array $paths): Vault
142
    {
143
        $this->exclusions = array_values($paths);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_values($paths) of type array<integer,?> is incompatible with the declared type array<integer,string> of property $exclusions.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
144
145
        return $this;
146
    }
147
148
    public function getIdentity(): string
149
    {
150
        return $this->identity;
151
    }
152
153
    public function setIdentity(string $identity = null): Vault
154
    {
155
        $this->identity = $identity;
156
157
        return $this;
158
    }
159
160
    /**
161
     * Builds and returns an index representing the current local state.
162
     *
163
     * @return Index
164
     */
165
    public function buildLocalIndex(): Index
166
    {
167
        $finder = new Finder();
168
        $finder->in($this->localPath);
169
        $finder->ignoreDotFiles(false);
170
        $finder->ignoreVCS(true);
171
        $finder->exclude(static::METADATA_DIRECTORY_NAME);
172
        $finder->notPath('archivr.json');
173
174
        foreach ($this->exclusions as $path)
175
        {
176
            $finder->notPath($path);
177
        }
178
179
        $index = new Index();
180
181
        foreach ($finder->directories() as $fileInfo)
182
        {
183
            /** @var SplFileInfo $fileInfo */
184
185
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
186
        }
187
188
        foreach ($finder->files() as $fileInfo)
189
        {
190
            /** @var SplFileInfo $fileInfo */
191
192
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
193
        }
194
195
        return $index;
196
    }
197
198
    /**
199
     * Reads and returns the index representing the local state on the last synchronization.
200
     *
201
     * @return Index
202
     * @throws Exception
203
     */
204
    public function loadLastLocalIndex()
205
    {
206
        $index = null;
207
        $path = $this->getLastLocalIndexFilePath();
208
209
        if (is_file($path))
210
        {
211
            $stream = fopen($path, 'rb');
212
            $indexModificationDate = \DateTime::createFromFormat('U', filemtime($path));
213
214
            if (!($indexModificationDate instanceof \DateTime))
215
            {
216
                throw new Exception();
217
            }
218
219
            $index = $this->readIndexFromStream($stream, $indexModificationDate);
220
221
            fclose($stream);
222
        }
223
224
        return $index;
225
    }
226
227
    /**
228
     * Reads and returns the current remote index.
229
     *
230
     * @param int $revision Revision to load. Defaults to the last revision.
231
     *
232
     * @return Index
233
     */
234
    public function loadRemoteIndex(int $revision = null)
235
    {
236
        $list = null;
237
238 View Code Duplication
        if ($revision === null)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
239
        {
240
            $list = $this->loadSynchronizationList();
241
242
            if (!$list->getLastSynchronization())
243
            {
244
                return null;
245
            }
246
247
            $revision = $list->getLastSynchronization()->getRevision();
248
        }
249
250
        return $this->doLoadRemoteIndex($revision, $list);
251
    }
252
253
    /**
254
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
255
     *
256
     * @return Index
257
     */
258
    public function buildMergedIndex(): Index
259
    {
260
        return $this->doBuildMergedIndex();
261
    }
262
263
    /**
264
     * Returns ordered collection of operations required to synchronize the vault with the local path.
265
     * In addition to the object specific operations contained in the returned OperationCollection additional operations
266
     * might be necessary like index updates that do not belong to specific index objects.
267
     *
268
     * @return OperationCollection
269
     */
270
    public function getOperationCollection(): OperationCollection
271
    {
272
        return $this->doGetOperationCollection();
273
    }
274
275
    /**
276
     * Synchronizes the local with the remote state by executing all operations returned by getOperationCollection() (broadly speaking).
277
     *
278
     * @param int $newRevision
279
     * @param bool $preferLocal
280
     * @param SynchronizationProgressListenerInterface $progressionListener
281
     *
282
     * @return OperationResultCollection
283
     * @throws Exception
284
     */
285
    public function synchronize(int $newRevision = null, bool $preferLocal = false, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultCollection
286
    {
287
        if ($progressionListener === null)
288
        {
289
            $progressionListener = new DummySynchronizationProgressListener();
290
        }
291
292
        $localIndex = $this->buildLocalIndex();
293
        $lastLocalIndex = $this->loadLastLocalIndex();
294
295
296
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
297
        {
298
            throw new Exception('Failed to acquire lock.');
299
        }
300
301
302
        $synchronizationList = $this->loadSynchronizationList();
303
        $lastSynchronization = $synchronizationList->getLastSynchronization();
304
305
        if ($lastSynchronization)
306
        {
307
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
308
            $remoteIndex = $this->doLoadRemoteIndex($lastSynchronization->getRevision(), $synchronizationList);
309
        }
310
        else
311
        {
312
            $newRevision = $newRevision ?: 1;
313
            $remoteIndex = null;
314
        }
315
316
        $synchronization = new Synchronization($newRevision, $this->generateNewBlobId(), new \DateTime(), $this->identity);
317
        $synchronizationList->addSynchronization($synchronization);
318
319
        // don't merge indices but just use local
320
        if ($preferLocal)
321
        {
322
            $mergedIndex = $localIndex;
323
        }
324
325
        // compute merged index
326
        else
327
        {
328
            $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
329
        }
330
331
        $operationCollection = $this->doGetOperationCollection($localIndex, $remoteIndex, $mergedIndex);
332
333
        $operationResultCollection = new OperationResultCollection();
334
335
        // operation count +
336
        // merged index write +
337
        // copy merged index to vault +
338
        // save merged index as last local index +
339
        // upload synchronization list +
340
        // release lock
341
        $progressionListener->start(count($operationCollection) + 5);
342
343 View Code Duplication
        foreach ($operationCollection as $operation)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
344
        {
345
            /** @var OperationInterface $operation */
346
347
            $success = $operation->execute();
348
349
            $operationResult = new OperationResult($operation, $success);
350
            $operationResultCollection->addOperationResult($operationResult);
351
352
            $progressionListener->advance();
353
        }
354
355
        // dump new index
356
        $mergedIndexFilePath = tempnam(sys_get_temp_dir(), 'index');
357
        $this->writeIndexToFile($mergedIndex, $mergedIndexFilePath);
358
359
        $progressionListener->advance();
360
361
        // upload new index
362
        $readStream = fopen($mergedIndexFilePath, 'rb');
363
        $compressionFilter = stream_filter_append($readStream, 'zlib.deflate');
364
        $this->vaultConnection->writeStream($synchronization->getBlobId(), $readStream);
365
        rewind($readStream);
366
        stream_filter_remove($compressionFilter);
367
368
        $progressionListener->advance();
369
370
        // save new index locally
371
        $writeStream = fopen($this->getLastLocalIndexFilePath(), 'wb');
372
        stream_copy_to_stream($readStream, $writeStream);
373
        fclose($writeStream);
374
        fclose($readStream);
375
376
        $progressionListener->advance();
377
378
        // upload new synchronization list
379
        $synchronizationListFilePath = $this->writeSynchronizationListToTemporaryFile($synchronizationList);
380
        $readStream = fopen($synchronizationListFilePath, 'rb');
381
        stream_filter_append($readStream, 'zlib.deflate');
382
        $this->vaultConnection->writeStream(static::SYNCHRONIZATION_LIST_FILE_NAME, $readStream);
383
        fclose($readStream);
384
385
        // release lock
386
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
387
        {
388
            throw new Exception('Failed to release lock.');
389
        }
390
391
        $progressionListener->advance();
392
        $progressionListener->finish();
393
394
        return $operationResultCollection;
395
    }
396
397
    /**
398
     * Loads and returns the list of synchronizations from the vault.
399
     *
400
     * @return SynchronizationList
401
     */
402
    public function loadSynchronizationList(): SynchronizationList
403
    {
404
        $list = null;
0 ignored issues
show
Unused Code introduced by
$list is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
405
406
        if ($this->vaultConnection->exists(static::SYNCHRONIZATION_LIST_FILE_NAME))
407
        {
408
            $stream = $this->vaultConnection->getReadStream(static::SYNCHRONIZATION_LIST_FILE_NAME);
409
410
            stream_filter_append($stream, 'zlib.inflate');
411
412
            $list = $this->readSynchronizationListFromStream($stream);
413
414
            fclose($stream);
415
416
            return $list;
417
        }
418
419
        return new SynchronizationList();
420
    }
421
422
    /**
423
     * Restores the local state at the given revision from the vault.
424
     *
425
     * @param int $revision
426
     * @param SynchronizationProgressListenerInterface $progressionListener
427
     *
428
     * @return OperationResultCollection
429
     * @throws Exception
430
     */
431
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultCollection
432
    {
433
        return $this->doRestore($revision, $progressionListener);
434
    }
435
436
    /**
437
     * @param string $targetPath
438
     * @param int $revision
439
     * @param SynchronizationProgressListenerInterface|null $progressListener
440
     *
441
     * @return OperationResultCollection
442
     * @throws \Exception
443
     */
444
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultCollection
445
    {
446
        $originalLocalPath = $this->localPath;
447
        $this->localPath =  rtrim($this->expandTildePath($targetPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
448
449
        try
450
        {
451
            return $this->doRestore($revision, $progressListener, true);
452
        }
453
        catch (\Exception $exception)
454
        {
455
            throw $exception;
456
        }
457
        finally
458
        {
459
            $this->localPath = $originalLocalPath;
460
        }
461
    }
462
463
    protected function doLoadRemoteIndex(int $revision, SynchronizationList $synchronizationList = null)
464
    {
465
        if ($synchronizationList === null)
466
        {
467
            $synchronizationList = $this->loadSynchronizationList();
468
        }
469
470
        $synchronization = $synchronizationList->getSynchronizationByRevision($revision);
471
472
        if (!$synchronization)
473
        {
474
            return null;
475
        }
476
477
        $index = null;
478
479
        if ($this->vaultConnection->exists($synchronization->getBlobId()))
480
        {
481
            $stream = $this->vaultConnection->getReadStream($synchronization->getBlobId());
482
483
            stream_filter_append($stream, 'zlib.inflate');
484
485
            $index = $this->readIndexFromStream($stream);
486
487
            fclose($stream);
488
        }
489
490
        return $index;
491
    }
492
493
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null)
494
    {
495
        $localIndex = $localIndex ?: $this->buildLocalIndex();
496
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
497
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
498
499
        if ($remoteIndex === null)
500
        {
501
            return $localIndex;
502
        }
503
504
        return $this->getIndexMerger()->merge($remoteIndex, $localIndex, $lastLocalIndex);
505
    }
506
507
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false): OperationResultCollection
508
    {
509
        if ($progressionListener === null)
510
        {
511
            $progressionListener = new DummySynchronizationProgressListener();
512
        }
513
514
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
515
        {
516
            throw new Exception('Failed to acquire lock.');
517
        }
518
519 View Code Duplication
        if ($revision === null)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
520
        {
521
            $synchronizationList = $this->loadSynchronizationList();
522
523
            if (!$synchronizationList->getLastSynchronization())
524
            {
525
                throw new Exception('No revision to restore from.');
526
            }
527
528
            $revision = $synchronizationList->getLastSynchronization()->getRevision();
529
        }
530
531
        $remoteIndex = $this->loadRemoteIndex($revision);
532
533
        if ($remoteIndex === null)
534
        {
535
            throw new Exception("Unknown revision: {$revision}");
536
        }
537
538
        $operationCollection = $this->doGetOperationCollection(null, $remoteIndex, $remoteIndex);
539
540
        $operationResultCollection = new OperationResultCollection();
541
542
        // operation count +
543
        // save merged index as last local index +
544
        // release lock
545
        $progressionListener->start(count($operationCollection) + 2);
546
547 View Code Duplication
        foreach ($operationCollection as $operation)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
548
        {
549
            /** @var OperationInterface $operation */
550
551
            $success = $operation->execute();
552
553
            $operationResult = new OperationResult($operation, $success);
554
            $operationResultCollection->addOperationResult($operationResult);
555
556
            $progressionListener->advance();
557
        }
558
559
        if (!$skipLastLocalIndexUpdate)
560
        {
561
            $this->writeIndexToFile($remoteIndex, $this->getLastLocalIndexFilePath());
562
        }
563
564
        $progressionListener->advance();
565
566
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
567
        {
568
            throw new Exception('Failed to release lock.');
569
        }
570
571
        $progressionListener->advance();
572
        $progressionListener->finish();
573
574
        return $operationResultCollection;
575
    }
576
577
    protected function doGetOperationCollection(Index $localIndex = null, Index $remoteIndex = null, Index $mergedIndex = null): OperationCollection
578
    {
579
        $noUpload = $localIndex === null;
580
        $localIndex = $localIndex ?: $this->buildLocalIndex();
581
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
582
        $mergedIndex = $mergedIndex ?: $this->doBuildMergedIndex($localIndex, $this->loadLastLocalIndex(), $remoteIndex);
583
584
        $uploadStreamFilters = [
585
            'zlib.deflate' => []
586
        ];
587
        $downloadStreamFilters = [
588
            'zlib.inflate' => []
589
        ];
590
591
592
        $operationCollection = new OperationCollection();
593
594
        // mtimes to be set for directories are collected and applied afterwards as they get modified by synchronization operations as well
595
        $directoryMtimes = [];
596
597
        // relies on the directory tree structure being traversed in pre-order (or at least a directory appears before its content)
598
        foreach ($mergedIndex as $mergedIndexObject)
599
        {
600
            /** @var IndexObject $mergedIndexObject */
601
602
            $absoluteLocalPath = $this->localPath . $mergedIndexObject->getRelativePath();
603
604
            $localObject = $localIndex->getObjectByPath($mergedIndexObject->getRelativePath());
605
            $remoteObject = $remoteIndex ? $remoteIndex->getObjectByPath($mergedIndexObject->getRelativePath()) : null;
606
607
            // unlink to-be-overridden local path with different type
608
            if ($localObject !== null && $localObject->getType() !== $mergedIndexObject->getType())
609
            {
610
                $operationCollection->addOperation(new UnlinkOperation($absoluteLocalPath));
611
            }
612
613
614
            if ($mergedIndexObject->isDirectory())
615
            {
616
                if ($localObject === null || !$localObject->isDirectory())
617
                {
618
                    $operationCollection->addOperation(new MkdirOperation($absoluteLocalPath, $mergedIndexObject->getMode()));
619
                }
620
621
                if ($localObject !== null && $localObject->isDirectory())
622
                {
623
                    if ($localObject->getMtime() !== $mergedIndexObject->getMtime())
624
                    {
625
                        $directoryMtimes[$absoluteLocalPath] = $mergedIndexObject->getMtime();
626
                    }
627
                }
628
            }
629
630
            elseif ($mergedIndexObject->isFile())
631
            {
632
                // local file did not exist, hasn't been a file before or is outdated
633
                $doDownloadFile = $localObject === null || !$localObject->isFile() || $localObject->getMtime() < $mergedIndexObject->getMtime();
634
635
                // file has to be restored as it does not equal the local version
636
                $doDownloadFile |= $noUpload && $localObject !== null && $mergedIndexObject->getBlobId() !== $localObject->getBlobId();
637
638
                if ($doDownloadFile)
639
                {
640
                    $operationCollection->addOperation(new DownloadOperation($absoluteLocalPath, $mergedIndexObject->getBlobId(), $this->vaultConnection, $downloadStreamFilters));
641
                    $operationCollection->addOperation(new TouchOperation($absoluteLocalPath, $mergedIndexObject->getMtime()));
642
                    $operationCollection->addOperation(new ChmodOperation($absoluteLocalPath, $mergedIndexObject->getMode()));
643
644
                    $directoryMtimes[dirname($absoluteLocalPath)] = $mergedIndexObject->getMtime();
645
                }
646
647
                // local file got created or updated
648
                elseif ($remoteObject === null || $mergedIndexObject->getBlobId() === null)
649
                {
650
                    // generate blob id
651
                    do
652
                    {
653
                        $newBlobId = $mergedIndex->generateNewBlobId();
654
                    }
655
                    while ($this->vaultConnection->exists($newBlobId));
656
657
                    $mergedIndexObject->setBlobId($newBlobId);
658
659
                    $operationCollection->addOperation(new UploadOperation($absoluteLocalPath, $mergedIndexObject->getBlobId(), $this->vaultConnection, $uploadStreamFilters));
660
                }
661
            }
662
663
            elseif ($mergedIndexObject->isLink())
664
            {
665
                $absoluteLinkTarget = dirname($absoluteLocalPath) . DIRECTORY_SEPARATOR . $mergedIndexObject->getLinkTarget();
666
667
                if ($localObject !== null && $localObject->getLinkTarget() !== $mergedIndexObject->getLinkTarget())
668
                {
669
                    $operationCollection->addOperation(new UnlinkOperation($absoluteLocalPath));
670
                    $operationCollection->addOperation(new SymlinkOperation($absoluteLocalPath, $absoluteLinkTarget, $mergedIndexObject->getMode()));
671
                }
672
            }
673
674
            else
675
            {
676
                // unknown/invalid object type
677
                throw new Exception();
678
            }
679
680
681
            if ($localObject !== null && $localObject->getMode() !== $mergedIndexObject->getMode())
682
            {
683
                $operationCollection->addOperation(new ChmodOperation($absoluteLocalPath, $mergedIndexObject->getMode()));
684
            }
685
        }
686
687
        // remove superfluous local files
688
        foreach ($localIndex as $localObject)
689
        {
690
            /** @var IndexObject $localObject */
691
692
            if ($mergedIndex->getObjectByPath($localObject->getRelativePath()) === null)
693
            {
694
                $operationCollection->addOperation(new UnlinkOperation($this->localPath . $localObject->getRelativePath()));
695
            }
696
        }
697
698
        // set directory mtimes after all other modifications have been performed
699
        foreach ($directoryMtimes as $absoluteLocalPath => $mtime)
700
        {
701
            $operationCollection->addOperation(new TouchOperation($absoluteLocalPath, $mtime));
702
        }
703
704
        return $operationCollection;
705
    }
706
707
    protected function readIndexFromStream($stream, \DateTime $created = null): Index
708
    {
709
        if (!is_resource($stream))
710
        {
711
            throw new Exception();
712
        }
713
714
        $index = new Index($created);
715
716
        while (($row = fgetcsv($stream)) !== false)
717
        {
718
            $index->addObject(IndexObject::fromIndexRecord($row));
719
        }
720
721
        return $index;
722
    }
723
724
    protected function writeIndexToFile(Index $index, string $path)
725
    {
726
        $stream = fopen($path, 'wb');
727
728
        foreach ($index as $object)
729
        {
730
            /** @var IndexObject $object */
731
732
            if (fputcsv($stream, $object->getIndexRecord()) === false)
733
            {
734
                throw new Exception();
735
            }
736
        }
737
738
        fclose($stream);
739
    }
740
741
    protected function readSynchronizationListFromStream($stream): SynchronizationList
742
    {
743
        if (!is_resource($stream))
744
        {
745
            throw new Exception();
746
        }
747
748
        $list = new SynchronizationList();
749
750
        while (($row = fgetcsv($stream)) !== false)
751
        {
752
            $list->addSynchronization(Synchronization::fromRecord($row));
753
        }
754
755
        return $list;
756
    }
757
758
    protected function writeSynchronizationListToTemporaryFile(SynchronizationList $synchronizationList): string
759
    {
760
        $path = tempnam(sys_get_temp_dir(), 'synchronizationList');
761
        $stream = fopen($path, 'wb');
762
763
        foreach ($synchronizationList as $synchronization)
764
        {
765
            /** @var Synchronization $synchronization */
766
767
            if (fputcsv($stream, $synchronization->getRecord()) === false)
768
            {
769
                throw new Exception();
770
            }
771
        }
772
773
        fclose($stream);
774
775
        return $path;
776
    }
777
778
    protected function generateNewBlobId(): string
779
    {
780
        do
781
        {
782
            $blobId = Uuid::uuid4()->toString();
783
        }
784
        while ($this->vaultConnection->exists($blobId));
785
786
        return $blobId;
787
    }
788
789
    protected function initMetadataDirectory(): string
790
    {
791
        $path = $this->localPath . static::METADATA_DIRECTORY_NAME;
792
793
        if (!is_dir($path))
794
        {
795
            if (!mkdir($path))
796
            {
797
                throw new Exception();
798
            }
799
        }
800
801
        return $path . DIRECTORY_SEPARATOR;
802
    }
803
804
    protected function getLastLocalIndexFilePath(): string
805
    {
806
        return $this->initMetadataDirectory() . sprintf('lastLocalIndex-%s', $this->getTitle());
807
    }
808
}
809