Completed
Push — master ( f2b67d...98a2b4 )
by Arne
01:46
created

Vault::synchronize()   C

Complexity

Conditions 8
Paths 34

Size

Total Lines 101
Code Lines 50

Duplication

Lines 11
Ratio 10.89 %

Importance

Changes 0
Metric Value
dl 11
loc 101
rs 5.2676
c 0
b 0
f 0
cc 8
eloc 50
nc 34
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 Ramsey\Uuid\Uuid;
12
use Symfony\Component\Finder\Finder;
13
use Symfony\Component\Finder\SplFileInfo;
14
use Archivr\Operation\ChmodOperation;
15
use Archivr\Operation\DownloadOperation;
16
use Archivr\Operation\MkdirOperation;
17
use Archivr\Operation\OperationInterface;
18
use Archivr\Operation\SymlinkOperation;
19
use Archivr\Operation\TouchOperation;
20
use Archivr\Operation\UnlinkOperation;
21
use Archivr\Operation\UploadOperation;
22
23
class Vault
24
{
25
    use TildeExpansionTrait;
26
27
    const METADATA_DIRECTORY_NAME = '.archivr';
28
    const SYNCHRONIZATION_LIST_FILE_NAME = 'index';
29
    const LOCK_SYNC = 'sync';
30
31
    /**
32
     * @var string
33
     */
34
    protected $title;
35
36
    /**
37
     * @var ConnectionAdapterInterface
38
     */
39
    protected $vaultConnection;
40
41
    /**
42
     * @var string
43
     */
44
    protected $localPath;
45
46
    /**
47
     * @var LockAdapterInterface
48
     */
49
    protected $lockAdapter;
50
51
    /**
52
     * @var IndexMergerInterface
53
     */
54
    protected $indexMerger;
55
56
    /**
57
     * @var string[]
58
     */
59
    protected $exclusions = [];
60
61
    /**
62
     * @var string
63
     */
64
    protected $identity;
65
66
67
    public function __construct(string $title, string $localPath, ConnectionAdapterInterface $vaultConnection)
68
    {
69
        $this->title = $title;
70
        $this->vaultConnection = $vaultConnection;
71
        $this->localPath = rtrim($this->expandTildePath($localPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
72
    }
73
74
    public function getTitle(): string
75
    {
76
        return $this->title;
77
    }
78
79
    public function getLocalPath(): string
80
    {
81
        return $this->localPath;
82
    }
83
84
    public function setIndexMerger(IndexMergerInterface $indexMerger = null)
85
    {
86
        $this->indexMerger = $indexMerger;
87
88
        return $this;
89
    }
90
91
    public function getIndexMerger(): IndexMergerInterface
92
    {
93
        if ($this->indexMerger === null)
94
        {
95
            $this->indexMerger = new StandardIndexMerger();
96
        }
97
98
        return $this->indexMerger;
99
    }
100
101
    public function setLockAdapter(LockAdapterInterface $lockAdapter = null): Vault
102
    {
103
        $this->lockAdapter = $lockAdapter;
104
105
        return $this;
106
    }
107
108
    public function getLockAdapter(): LockAdapterInterface
109
    {
110
        if ($this->lockAdapter === null)
111
        {
112
            $this->lockAdapter = new ConnectionBasedLockAdapter($this->vaultConnection);
113
        }
114
115
        return $this->lockAdapter;
116
    }
117
118
    public function getVaultConnection(): ConnectionAdapterInterface
119
    {
120
        return $this->vaultConnection;
121
    }
122
123
    public function getExclusions(): array
124
    {
125
        return $this->exclusions;
126
    }
127
128
    public function addExclusion(string $path): Vault
129
    {
130
        $this->exclusions[] = $path;
131
132
        return $this;
133
    }
134
135
    public function setExclusions(array $paths): Vault
136
    {
137
        $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...
138
139
        return $this;
140
    }
141
142
    public function getIdentity(): string
143
    {
144
        return $this->identity;
145
    }
146
147
    public function setIdentity(string $identity = null): Vault
148
    {
149
        $this->identity = $identity;
150
151
        return $this;
152
    }
153
154
    /**
155
     * Builds and returns an index representing the current local state.
156
     *
157
     * @return Index
158
     */
159
    public function buildLocalIndex(): Index
160
    {
161
        $finder = new Finder();
162
        $finder->in($this->localPath);
163
        $finder->ignoreDotFiles(false);
164
        $finder->ignoreVCS(true);
165
        $finder->exclude(static::METADATA_DIRECTORY_NAME);
166
        $finder->notPath('archivr.json');
167
168
        foreach ($this->exclusions as $path)
169
        {
170
            $finder->notPath($path);
171
        }
172
173
        $index = new Index();
174
175
        foreach ($finder->directories() as $fileInfo)
176
        {
177
            /** @var SplFileInfo $fileInfo */
178
179
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
180
        }
181
182
        foreach ($finder->files() as $fileInfo)
183
        {
184
            /** @var SplFileInfo $fileInfo */
185
186
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
187
        }
188
189
        return $index;
190
    }
191
192
    /**
193
     * Reads and returns the index representing the local state on the last synchronization.
194
     *
195
     * @return Index
196
     * @throws Exception
197
     */
198
    public function loadLastLocalIndex()
199
    {
200
        $index = null;
201
        $path = $this->getLastLocalIndexFilePath();
202
203
        if (is_file($path))
204
        {
205
            $stream = fopen($path, 'rb');
206
            $indexModificationDate = \DateTime::createFromFormat('U', filemtime($path));
207
208
            if (!($indexModificationDate instanceof \DateTime))
209
            {
210
                throw new Exception();
211
            }
212
213
            $index = $this->readIndexFromStream($stream, $indexModificationDate);
214
215
            fclose($stream);
216
        }
217
218
        return $index;
219
    }
220
221
    /**
222
     * Reads and returns the current remote index.
223
     *
224
     * @param int $revision Revision to load. Defaults to the last revision.
225
     *
226
     * @return Index
227
     */
228
    public function loadRemoteIndex(int $revision = null)
229
    {
230
        $list = null;
231
232 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...
233
        {
234
            $list = $this->loadSynchronizationList();
235
236
            if (!$list->getLastSynchronization())
237
            {
238
                return null;
239
            }
240
241
            $revision = $list->getLastSynchronization()->getRevision();
242
        }
243
244
        return $this->doLoadRemoteIndex($revision, $list);
245
    }
246
247
    /**
248
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
249
     *
250
     * @return Index
251
     */
252
    public function buildMergedIndex(): Index
253
    {
254
        return $this->doBuildMergedIndex();
255
    }
256
257
    /**
258
     * Returns ordered collection of operations required to synchronize the vault with the local path.
259
     * In addition to the object specific operations contained in the returned OperationCollection additional operations
260
     * might be necessary like index updates that do not belong to specific index objects.
261
     *
262
     * @return OperationCollection
263
     */
264
    public function getOperationCollection(): OperationCollection
265
    {
266
        return $this->doGetOperationCollection();
267
    }
268
269
    /**
270
     * Synchronizes the local with the remote state by executing all operations returned by getOperationCollection() (broadly speaking).
271
     *
272
     * @param int $newRevision
273
     * @param SynchronizationProgressListenerInterface $progressionListener
274
     *
275
     * @return OperationResultCollection
276
     * @throws Exception
277
     */
278
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultCollection
279
    {
280
        if ($progressionListener === null)
281
        {
282
            $progressionListener = new DummySynchronizationProgressListener();
283
        }
284
285
        $localIndex = $this->buildLocalIndex();
286
        $lastLocalIndex = $this->loadLastLocalIndex();
287
288
289
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
290
        {
291
            throw new Exception('Failed to acquire lock.');
292
        }
293
294
295
        $synchronizationList = $this->loadSynchronizationList();
296
        $lastSynchronization = $synchronizationList->getLastSynchronization();
297
298
        if ($lastSynchronization)
299
        {
300
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
301
            $remoteIndex = $this->doLoadRemoteIndex($lastSynchronization->getRevision(), $synchronizationList);
302
        }
303
        else
304
        {
305
            $newRevision = $newRevision ?: 1;
306
            $remoteIndex = null;
307
        }
308
309
        $synchronization = new Synchronization($newRevision, $this->generateNewBlobId(), new \DateTime(), $this->identity);
310
        $synchronizationList->addSynchronization($synchronization);
311
312
313
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
314
        $operationCollection = $this->doGetOperationCollection($localIndex, $remoteIndex, $mergedIndex);
315
316
        $operationResultCollection = new OperationResultCollection();
317
318
        // operation count +
319
        // merged index write +
320
        // copy merged index to vault +
321
        // save merged index as last local index +
322
        // upload synchronization list +
323
        // release lock
324
        $progressionListener->start(count($operationCollection) + 5);
325
326 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...
327
        {
328
            /** @var OperationInterface $operation */
329
330
            $success = $operation->execute();
331
332
            $operationResult = new OperationResult($operation, $success);
333
            $operationResultCollection->addOperationResult($operationResult);
334
335
            $progressionListener->advance();
336
        }
337
338
        // dump new index
339
        $mergedIndexFilePath = tempnam(sys_get_temp_dir(), 'index');
340
        $this->writeIndexToFile($mergedIndex, $mergedIndexFilePath);
341
342
        $progressionListener->advance();
343
344
        // upload new index
345
        $readStream = fopen($mergedIndexFilePath, 'rb');
346
        $compressionFilter = stream_filter_append($readStream, 'zlib.deflate');
347
        $this->vaultConnection->writeStream($synchronization->getBlobId(), $readStream);
348
        rewind($readStream);
349
        stream_filter_remove($compressionFilter);
350
351
        $progressionListener->advance();
352
353
        // save new index locally
354
        $writeStream = fopen($this->getLastLocalIndexFilePath(), 'wb');
355
        stream_copy_to_stream($readStream, $writeStream);
356
        fclose($writeStream);
357
        fclose($readStream);
358
359
        $progressionListener->advance();
360
361
        // upload new synchronization list
362
        $synchronizationListFilePath = $this->writeSynchronizationListToTemporaryFile($synchronizationList);
363
        $readStream = fopen($synchronizationListFilePath, 'rb');
364
        stream_filter_append($readStream, 'zlib.deflate');
365
        $this->vaultConnection->writeStream(static::SYNCHRONIZATION_LIST_FILE_NAME, $readStream);
366
        fclose($readStream);
367
368
        // release lock
369
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
370
        {
371
            throw new Exception('Failed to release lock.');
372
        }
373
374
        $progressionListener->advance();
375
        $progressionListener->finish();
376
377
        return $operationResultCollection;
378
    }
379
380
    /**
381
     * Loads and returns the list of synchronizations from the vault.
382
     *
383
     * @return SynchronizationList
384
     */
385
    public function loadSynchronizationList(): SynchronizationList
386
    {
387
        $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...
388
389
        if ($this->vaultConnection->exists(static::SYNCHRONIZATION_LIST_FILE_NAME))
390
        {
391
            $stream = $this->vaultConnection->getReadStream(static::SYNCHRONIZATION_LIST_FILE_NAME);
392
393
            stream_filter_append($stream, 'zlib.inflate');
394
395
            $list = $this->readSynchronizationListFromStream($stream);
396
397
            fclose($stream);
398
399
            return $list;
400
        }
401
402
        return new SynchronizationList();
403
    }
404
405
    /**
406
     * Restores the local state at the given revision from the vault.
407
     *
408
     * @param int $revision
409
     * @param SynchronizationProgressListenerInterface $progressionListener
410
     *
411
     * @return OperationResultCollection
412
     * @throws Exception
413
     */
414
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultCollection
415
    {
416
        return $this->doRestore($revision, $progressionListener);
417
    }
418
419
    /**
420
     * @param string $targetPath
421
     * @param int $revision
422
     * @param SynchronizationProgressListenerInterface|null $progressListener
423
     *
424
     * @return OperationResultCollection
425
     * @throws \Exception
426
     */
427
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultCollection
428
    {
429
        $originalLocalPath = $this->localPath;
430
        $this->localPath = $targetPath;
431
432
        try
433
        {
434
            return $this->doRestore($revision, $progressListener, true);
435
        }
436
        catch (\Exception $exception)
437
        {
438
            throw $exception;
439
        }
440
        finally
441
        {
442
            $this->localPath = $originalLocalPath;
443
        }
444
    }
445
446
    protected function doLoadRemoteIndex(int $revision, SynchronizationList $synchronizationList = null)
447
    {
448
        if ($synchronizationList === null)
449
        {
450
            $synchronizationList = $this->loadSynchronizationList();
451
        }
452
453
        $synchronization = $synchronizationList->getSynchronizationByRevision($revision);
454
455
        if (!$synchronization)
456
        {
457
            return null;
458
        }
459
460
        $index = null;
461
462
        if ($this->vaultConnection->exists($synchronization->getBlobId()))
463
        {
464
            $stream = $this->vaultConnection->getReadStream($synchronization->getBlobId());
465
466
            stream_filter_append($stream, 'zlib.inflate');
467
468
            $index = $this->readIndexFromStream($stream);
469
470
            fclose($stream);
471
        }
472
473
        return $index;
474
    }
475
476
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null)
477
    {
478
        $localIndex = $localIndex ?: $this->buildLocalIndex();
479
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
480
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
481
482
        return $this->getIndexMerger()->merge($localIndex, $lastLocalIndex, $remoteIndex);
483
    }
484
485
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false): OperationResultCollection
486
    {
487
        if ($progressionListener === null)
488
        {
489
            $progressionListener = new DummySynchronizationProgressListener();
490
        }
491
492
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
493
        {
494
            throw new Exception('Failed to acquire lock.');
495
        }
496
497 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...
498
        {
499
            $synchronizationList = $this->loadSynchronizationList();
500
501
            if (!$synchronizationList->getLastSynchronization())
502
            {
503
                throw new Exception('No revision to restore from.');
504
            }
505
506
            $revision = $synchronizationList->getLastSynchronization()->getRevision();
507
        }
508
509
        $remoteIndex = $this->loadRemoteIndex($revision);
510
511
        if ($remoteIndex === null)
512
        {
513
            throw new Exception("Unknown revision: {$revision}");
514
        }
515
516
        $operationCollection = $this->doGetOperationCollection(null, $remoteIndex, $remoteIndex, true);
517
518
        $operationResultCollection = new OperationResultCollection();
519
520
        // operation count +
521
        // save merged index as last local index +
522
        // release lock
523
        $progressionListener->start(count($operationCollection) + 2);
524
525 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...
526
        {
527
            /** @var OperationInterface $operation */
528
529
            $success = $operation->execute();
530
531
            $operationResult = new OperationResult($operation, $success);
532
            $operationResultCollection->addOperationResult($operationResult);
533
534
            $progressionListener->advance();
535
        }
536
537
        if (!$skipLastLocalIndexUpdate)
538
        {
539
            $this->writeIndexToFile($remoteIndex, $this->getLastLocalIndexFilePath());
540
        }
541
542
        $progressionListener->advance();
543
544
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
545
        {
546
            throw new Exception('Failed to release lock.');
547
        }
548
549
        $progressionListener->advance();
550
        $progressionListener->finish();
551
552
        return $operationResultCollection;
553
    }
554
555
    protected function doGetOperationCollection(Index $localIndex = null, Index $remoteIndex = null, Index $mergedIndex = null, bool $restoreMode = false): OperationCollection
556
    {
557
        $localIndex = $localIndex ?: $this->buildLocalIndex();
558
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
559
        $mergedIndex = $mergedIndex ?: $this->doBuildMergedIndex($localIndex, $remoteIndex);
560
561
        $uploadStreamFilters = [
562
            'zlib.deflate' => []
563
        ];
564
        $downloadStreamFilters = [
565
            'zlib.inflate' => []
566
        ];
567
568
569
        $operationCollection = new OperationCollection();
570
571
        // mtimes to be set for directories are collected and applied afterwards as they get modified by synchronization operations as well
572
        $directoryMtimes = [];
573
574
        // relies on the directory tree structure being traversed in pre-order (or at least a directory appears before its content)
575
        foreach ($mergedIndex as $mergedIndexObject)
576
        {
577
            /** @var IndexObject $mergedIndexObject */
578
579
            $absoluteLocalPath = $this->localPath . $mergedIndexObject->getRelativePath();
580
581
            $localObject = $localIndex->getObjectByPath($mergedIndexObject->getRelativePath());
582
            $remoteObject = $remoteIndex ? $remoteIndex->getObjectByPath($mergedIndexObject->getRelativePath()) : null;
583
584
            // unlink to-be-overridden local path with different type
585
            if ($localObject !== null && $localObject->getType() !== $mergedIndexObject->getType())
586
            {
587
                $operationCollection->addOperation(new UnlinkOperation($absoluteLocalPath));
588
            }
589
590
591
            if ($mergedIndexObject->isDirectory())
592
            {
593
                if ($localObject === null || !$localObject->isDirectory())
594
                {
595
                    $operationCollection->addOperation(new MkdirOperation($absoluteLocalPath, $mergedIndexObject->getMode()));
596
                }
597
598
                if ($localObject !== null && $localObject->isDirectory())
599
                {
600
                    if ($localObject->getMtime() !== $mergedIndexObject->getMtime())
601
                    {
602
                        $directoryMtimes[$absoluteLocalPath] = $mergedIndexObject->getMtime();
603
                    }
604
                }
605
            }
606
607
            elseif ($mergedIndexObject->isFile())
608
            {
609
                // local file did not exist, hasn't been a file before or is outdated
610
                $doDownloadFile = $localObject === null || !$localObject->isFile() || $localObject->getMtime() < $mergedIndexObject->getMtime();
611
612
                // file has to be restored as it does not equal the local version
613
                $doDownloadFile |= $restoreMode && $localObject !== null && $mergedIndexObject->getBlobId() !== $localObject->getBlobId();
614
615
                if ($doDownloadFile)
616
                {
617
                    $operationCollection->addOperation(new DownloadOperation($absoluteLocalPath, $mergedIndexObject->getBlobId(), $this->vaultConnection, $downloadStreamFilters));
618
                    $operationCollection->addOperation(new TouchOperation($absoluteLocalPath, $mergedIndexObject->getMtime()));
619
                    $operationCollection->addOperation(new ChmodOperation($absoluteLocalPath, $mergedIndexObject->getMode()));
620
621
                    $directoryMtimes[dirname($absoluteLocalPath)] = $mergedIndexObject->getMtime();
622
                }
623
624
                // local file got created or updated
625
                elseif ($remoteObject === null || $mergedIndexObject->getBlobId() === null)
626
                {
627
                    // generate blob id
628
                    do
629
                    {
630
                        $newBlobId = $mergedIndex->generateNewBlobId();
631
                    }
632
                    while ($this->vaultConnection->exists($newBlobId));
633
634
                    $mergedIndexObject->setBlobId($newBlobId);
635
636
                    $operationCollection->addOperation(new UploadOperation($absoluteLocalPath, $mergedIndexObject->getBlobId(), $this->vaultConnection, $uploadStreamFilters));
637
                }
638
            }
639
640
            elseif ($mergedIndexObject->isLink())
641
            {
642
                $absoluteLinkTarget = dirname($absoluteLocalPath) . DIRECTORY_SEPARATOR . $mergedIndexObject->getLinkTarget();
643
644
                if ($localObject !== null && $localObject->getLinkTarget() !== $mergedIndexObject->getLinkTarget())
645
                {
646
                    $operationCollection->addOperation(new UnlinkOperation($absoluteLocalPath));
647
                    $operationCollection->addOperation(new SymlinkOperation($absoluteLocalPath, $absoluteLinkTarget, $mergedIndexObject->getMode()));
648
                }
649
            }
650
651
            else
652
            {
653
                // unknown/invalid object type
654
                throw new Exception();
655
            }
656
657
658
            if ($localObject !== null && $localObject->getMode() !== $mergedIndexObject->getMode())
659
            {
660
                $operationCollection->addOperation(new ChmodOperation($absoluteLocalPath, $mergedIndexObject->getMode()));
661
            }
662
        }
663
664
        // remove superfluous local files
665
        foreach ($localIndex as $localObject)
666
        {
667
            /** @var IndexObject $localObject */
668
669
            if ($mergedIndex->getObjectByPath($localObject->getRelativePath()) === null)
670
            {
671
                $operationCollection->addOperation(new UnlinkOperation($this->localPath . $localObject->getRelativePath()));
672
            }
673
        }
674
675
        // set directory mtimes after all other modifications have been performed
676
        foreach ($directoryMtimes as $absoluteLocalPath => $mtime)
677
        {
678
            $operationCollection->addOperation(new TouchOperation($absoluteLocalPath, $mtime));
679
        }
680
681
        return $operationCollection;
682
    }
683
684
    protected function readIndexFromStream($stream, \DateTime $created = null): Index
685
    {
686
        if (!is_resource($stream))
687
        {
688
            throw new Exception();
689
        }
690
691
        $index = new Index($created);
692
693
        while (($row = fgetcsv($stream)) !== false)
694
        {
695
            $index->addObject(IndexObject::fromIndexRecord($row));
696
        }
697
698
        return $index;
699
    }
700
701
    protected function writeIndexToFile(Index $index, string $path)
702
    {
703
        $stream = fopen($path, 'wb');
704
705
        foreach ($index as $object)
706
        {
707
            /** @var IndexObject $object */
708
709
            if (fputcsv($stream, $object->getIndexRecord()) === false)
710
            {
711
                throw new Exception();
712
            }
713
        }
714
715
        fclose($stream);
716
    }
717
718
    protected function readSynchronizationListFromStream($stream): SynchronizationList
719
    {
720
        if (!is_resource($stream))
721
        {
722
            throw new Exception();
723
        }
724
725
        $list = new SynchronizationList();
726
727
        while (($row = fgetcsv($stream)) !== false)
728
        {
729
            $list->addSynchronization(Synchronization::fromRecord($row));
730
        }
731
732
        return $list;
733
    }
734
735
    protected function writeSynchronizationListToTemporaryFile(SynchronizationList $synchronizationList): string
736
    {
737
        $path = tempnam(sys_get_temp_dir(), 'synchronizationList');
738
        $stream = fopen($path, 'wb');
739
740
        foreach ($synchronizationList as $synchronization)
741
        {
742
            /** @var Synchronization $synchronization */
743
744
            if (fputcsv($stream, $synchronization->getRecord()) === false)
745
            {
746
                throw new Exception();
747
            }
748
        }
749
750
        fclose($stream);
751
752
        return $path;
753
    }
754
755
    protected function generateNewBlobId(): string
756
    {
757
        do
758
        {
759
            $blobId = Uuid::uuid4()->toString();
760
        }
761
        while ($this->vaultConnection->exists($blobId));
762
763
        return $blobId;
764
    }
765
766
    protected function initMetadataDirectory(): string
767
    {
768
        $path = $this->localPath . static::METADATA_DIRECTORY_NAME;
769
770
        if (!is_dir($path))
771
        {
772
            if (!mkdir($path))
773
            {
774
                throw new Exception();
775
            }
776
        }
777
778
        return $path . DIRECTORY_SEPARATOR;
779
    }
780
781
    protected function getLastLocalIndexFilePath(): string
782
    {
783
        return $this->initMetadataDirectory() . sprintf('lastLocalIndex-%s', $this->getTitle());
784
    }
785
}
786