Completed
Push — master ( 83bd2d...9517e2 )
by Arne
03:10
created

Vault::writeIndexToStream()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 5
nc 3
nop 2
1
<?php
2
3
namespace Archivr;
4
5
use Archivr\ConnectionAdapter\ConnectionAdapterInterface;
6
use Archivr\IndexMerger\IndexMergerInterface;
7
use Archivr\IndexMerger\StandardIndexMerger;
8
use Archivr\LockAdapter\ConnectionBasedLockAdapter;
9
use Archivr\LockAdapter\LockAdapterInterface;
10
use Symfony\Component\Finder\Finder;
11
use Symfony\Component\Finder\SplFileInfo;
12
use Archivr\Operation\ChmodOperation;
13
use Archivr\Operation\DownloadOperation;
14
use Archivr\Operation\MkdirOperation;
15
use Archivr\Operation\OperationInterface;
16
use Archivr\Operation\SymlinkOperation;
17
use Archivr\Operation\TouchOperation;
18
use Archivr\Operation\UnlinkOperation;
19
use Archivr\Operation\UploadOperation;
20
21
class Vault
22
{
23
    use TildeExpansionTrait;
24
25
    const LAST_LOCAL_INDEX_FILE_NAME = '.lastLocalIndex';
26
    const REMOTE_INDEX_FILE_NAME = 'index';
27
28
    /**
29
     * @var ConnectionAdapterInterface
30
     */
31
    protected $vaultConnection;
32
33
    /**
34
     * @var string
35
     */
36
    protected $localPath;
37
38
    /**
39
     * @var LockAdapterInterface
40
     */
41
    protected $lockAdapter;
42
43
    /**
44
     * @var IndexMergerInterface
45
     */
46
    protected $indexMerger;
47
48
49
    public function __construct(string $localPath, ConnectionAdapterInterface $vaultConnection)
50
    {
51
        $this->vaultConnection = $vaultConnection;
52
        $this->localPath = rtrim($this->expandTildePath($localPath), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
53
    }
54
55
    public function setIndexMerger(IndexMergerInterface $indexMerger = null)
56
    {
57
        $this->indexMerger = $indexMerger;
58
59
        return $this;
60
    }
61
62
    public function getIndexMerger(): IndexMergerInterface
63
    {
64
        if ($this->indexMerger === null)
65
        {
66
            $this->indexMerger = new StandardIndexMerger();
67
        }
68
69
        return $this->indexMerger;
70
    }
71
72
    public function setLockAdapter(LockAdapterInterface $lockAdapter = null): Vault
73
    {
74
        $this->lockAdapter = $lockAdapter;
75
76
        return $this;
77
    }
78
79
    public function getLockAdapter(): LockAdapterInterface
80
    {
81
        if ($this->lockAdapter === null)
82
        {
83
            $this->lockAdapter = new ConnectionBasedLockAdapter($this->vaultConnection);
84
        }
85
86
        return $this->lockAdapter;
87
    }
88
89
    public function getVaultConnection(): ConnectionAdapterInterface
90
    {
91
        return $this->vaultConnection;
92
    }
93
94
    /**
95
     * Builds and returns an index representing the current local state.
96
     *
97
     * @return Index
98
     */
99
    public function buildLocalIndex(): Index
100
    {
101
        $finder = new Finder();
102
        $finder->in($this->localPath);
103
        $finder->ignoreDotFiles(false);
104
        $finder->ignoreVCS(true);
105
106
        $index = new Index();
107
108
        foreach ($finder->directories() as $fileInfo)
109
        {
110
            /** @var SplFileInfo $fileInfo */
111
112
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
113
        }
114
115
        foreach ($finder->files() as $fileInfo)
116
        {
117
            /** @var SplFileInfo $fileInfo */
118
119
            if ($fileInfo->getFilename() === Vault::LAST_LOCAL_INDEX_FILE_NAME)
120
            {
121
                continue;
122
            }
123
124
            $index->addObject(IndexObject::fromPath($this->localPath, $fileInfo->getRelativePathname()));
125
        }
126
127
        return $index;
128
    }
129
130
    /**
131
     * Reads and returns the index representing the local state on the last synchronization.
132
     *
133
     * @return Index
134
     */
135
    public function loadLastLocalIndex()
136
    {
137
        $index = null;
138
        $path = $this->localPath . self::LAST_LOCAL_INDEX_FILE_NAME;
139
140
        if (is_file($path))
141
        {
142
            $stream = fopen($path, 'r');
143
144
            $index = $this->readIndexFromStream($stream, \DateTime::createFromFormat('U', filemtime($path)));
0 ignored issues
show
Security Bug introduced by
It seems like \DateTime::createFromFor...('U', filemtime($path)) targeting DateTime::createFromFormat() can also be of type false; however, Archivr\Vault::readIndexFromStream() does only seem to accept null|object<DateTime>, did you maybe forget to handle an error condition?
Loading history...
145
146
            fclose($stream);
147
        }
148
149
        return $index;
150
    }
151
152
    /**
153
     * Reads and returns the current remote index.
154
     *
155
     * @return Index
156
     */
157
    public function loadRemoteIndex()
158
    {
159
        $index = null;
160
161
        if ($this->vaultConnection->exists(static::REMOTE_INDEX_FILE_NAME))
162
        {
163
            $stream = $this->vaultConnection->getReadStream(static::REMOTE_INDEX_FILE_NAME);
164
165
            $index = $this->readIndexFromStream($stream);
166
167
            fclose($stream);
168
        }
169
170
        return $index;
171
    }
172
173
    /**
174
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
175
     *
176
     * @return Index
177
     */
178
    public function buildMergedIndex(): Index
179
    {
180
        return $this->doBuildMergedIndex();
181
    }
182
183
    /**
184
     * Returns ordered collection of operations required to synchronize the vault with the local path.
185
     * In addition to the object specific operations contained in the returned OperationCollection additional operations
186
     * might be necessary like index updates that do not belong to specific index objects.
187
     *
188
     * @return OperationCollection
189
     */
190
    public function getOperationCollection(): OperationCollection
191
    {
192
        return $this->doGetOperationCollection();
193
    }
194
195
    /**
196
     * Synchronizes the local with the remote state by executing all operations returned by getOperationCollection() (broadly speaking).
197
     *
198
     * @param SynchronizationProgressListenerInterface $progressionListener
199
     *
200
     * @return OperationResultCollection
201
     */
202
    public function synchronize(SynchronizationProgressListenerInterface $progressionListener = null): OperationResultCollection
203
    {
204
        if ($progressionListener === null)
205
        {
206
            $progressionListener = new DummySynchronizationProgressListener();
207
        }
208
209
        $localIndex = $this->buildLocalIndex();
210
        $lastLocalIndex = $this->loadLastLocalIndex();
211
212
        // todo: ensure success
213
        $this->getLockAdapter()->acquireLock('sync');
214
215
        $remoteIndex = $this->loadRemoteIndex();
216
217
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
218
        $operationCollection = $this->doGetOperationCollection($localIndex, $remoteIndex, $mergedIndex);
219
220
        $operationResultCollection = new OperationResultCollection();
221
222
        // operation count +
223
        // merged index write +
224
        // copy merged index to vault +
225
        // save merged index as last local index +
226
        // release lock
227
        $progressionListener->start(count($operationCollection) + 4);
228
229
        foreach ($operationCollection as $operation)
230
        {
231
            /** @var OperationInterface $operation */
232
233
            $success = $operation->execute();
234
235
            $operationResult = new OperationResult($operation, $success);
236
            $operationResultCollection->addOperationResult($operationResult);
237
238
            $progressionListener->advance();
239
        }
240
241
        $mergedIndexFilePath = $this->writeIndexToTemporaryFile($mergedIndex);
242
243
        $progressionListener->advance();
244
245
        $readStream = fopen($mergedIndexFilePath, 'r');
246
        $this->vaultConnection->writeStream(static::REMOTE_INDEX_FILE_NAME, $readStream);
247
        rewind($readStream);
248
249
        $progressionListener->advance();
250
251
        $writeStream = fopen($this->localPath . self::LAST_LOCAL_INDEX_FILE_NAME, 'w');
252
        stream_copy_to_stream($readStream, $writeStream);
253
        fclose($writeStream);
254
        fclose($readStream);
255
256
        $progressionListener->advance();
257
258
        $this->getLockAdapter()->releaseLock('sync');
259
260
        $progressionListener->advance();
261
        $progressionListener->finish();
262
263
        return $operationResultCollection;
264
    }
265
266
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null)
267
    {
268
        $localIndex = $localIndex ?: $this->buildLocalIndex();
269
        $lastLocalIndex = $lastLocalIndex ?: $this->loadLastLocalIndex();
270
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
271
272
        return $this->getIndexMerger()->merge($localIndex, $lastLocalIndex, $remoteIndex);
273
    }
274
275
    protected function doGetOperationCollection(Index $localIndex = null, Index $remoteIndex = null, Index $mergedIndex = null): OperationCollection
276
    {
277
        $localIndex = $localIndex ?: $this->buildLocalIndex();
278
        $remoteIndex = $remoteIndex ?: $this->loadRemoteIndex();
279
        $mergedIndex = $mergedIndex ?: $this->doBuildMergedIndex($localIndex, $remoteIndex);
280
281
282
        $operationCollection = new OperationCollection();
283
284
        $directoryMtimes = [];
285
286
        foreach ($mergedIndex as $indexObject)
287
        {
288
            /** @var IndexObject $indexObject */
289
290
            $absoluteLocalPath = $this->localPath . $indexObject->getRelativePath();
291
292
            $localObject = $localIndex->getObjectByPath($indexObject->getRelativePath());
293
            $remoteObject = $remoteIndex ? $remoteIndex->getObjectByPath($indexObject->getRelativePath()) : null;
294
295
296
            if ($localObject !== null && $localObject->getType() !== $indexObject->getType())
297
            {
298
                $operationCollection->addOperation(new UnlinkOperation($absoluteLocalPath));
299
            }
300
301
302
            if ($indexObject->isDirectory())
303
            {
304
                if ($localObject === null)
305
                {
306
                    $operationCollection->addOperation(new MkdirOperation($absoluteLocalPath, $indexObject->getMode()));
307
                }
308
                elseif (!$localObject->isDirectory())
309
                {
310
                    $operationCollection->addOperation(new MkdirOperation($absoluteLocalPath, $indexObject->getMode()));
311
                }
312
313
                if ($localObject !== null && $localObject->isDirectory())
314
                {
315
                    if ($localObject->getMtime() !== $indexObject->getMtime())
316
                    {
317
                        $directoryMtimes[$absoluteLocalPath] = $indexObject->getMtime();
318
                    }
319
                }
320
            }
321
322
            elseif ($indexObject->isFile())
323
            {
324
                // local file did not exist, hasn't been a file before or is outdated
325
                if ($localObject === null || !$localObject->isFile() || $localObject->getMtime() < $indexObject->getMtime())
326
                {
327
                    $operationCollection->addOperation(new DownloadOperation($absoluteLocalPath, $indexObject->getBlobId(), $this->vaultConnection));
328
                    $operationCollection->addOperation(new TouchOperation($absoluteLocalPath, $indexObject->getMtime()));
329
                    $operationCollection->addOperation(new ChmodOperation($absoluteLocalPath, $indexObject->getMode()));
330
331
                    $directoryMtimes[dirname($absoluteLocalPath)] = $indexObject->getMtime();
332
                }
333
334
                // local file got updated
335
                elseif ($remoteObject === null || $indexObject->getBlobId() !== $remoteObject->getBlobId())
336
                {
337
                    // generate blob id
338
                    do
339
                    {
340
                        $blobId = $mergedIndex->generateNewBlobId();
341
                    }
342
                    while ($this->vaultConnection->exists($blobId));
343
344
                    $indexObject->setBlobId($blobId);
345
346
                    $operationCollection->addOperation(new UploadOperation($absoluteLocalPath, $indexObject->getBlobId(), $this->vaultConnection));
347
                }
348
            }
349
350
            elseif ($indexObject->isLink())
351
            {
352
                $absoluteLinkTarget = dirname($absoluteLocalPath) . DIRECTORY_SEPARATOR . $indexObject->getLinkTarget();
353
354
                if ($localObject !== null && $localObject->getLinkTarget() !== $indexObject->getLinkTarget())
355
                {
356
                    $operationCollection->addOperation(new UnlinkOperation($absoluteLocalPath));
357
                    $operationCollection->addOperation(new SymlinkOperation($absoluteLocalPath, $absoluteLinkTarget, $indexObject->getMode()));
358
                }
359
            }
360
361
            else
362
            {
363
                // unknown object type
364
                throw new \RuntimeException();
365
            }
366
367
368
            if ($localObject !== null && $localObject->getMode() !== $indexObject->getMode())
369
            {
370
                $operationCollection->addOperation(new ChmodOperation($absoluteLocalPath, $indexObject->getMode()));
371
            }
372
        }
373
374
        // remove superfluous local files
375
        foreach ($localIndex as $localObject)
376
        {
377
            /** @var IndexObject $localObject */
378
379
            if ($mergedIndex->getObjectByPath($localObject->getRelativePath()) === null)
380
            {
381
                $operationCollection->addOperation(new UnlinkOperation($this->localPath . $localObject->getRelativePath()));
382
            }
383
        }
384
385
        // set directory mtimes
386
        foreach ($directoryMtimes as $absoluteLocalPath => $mtime)
387
        {
388
            $operationCollection->addOperation(new TouchOperation($absoluteLocalPath, $mtime));
389
        }
390
391
        return $operationCollection;
392
    }
393
394
    protected function readIndexFromStream($stream, \DateTime $created = null): Index
395
    {
396
        $index = new Index($created);
397
398
        while (($row = fgetcsv($stream)) !== false)
399
        {
400
            $index->addObject(IndexObject::fromIndexRecord($row));
401
        }
402
403
        return $index;
404
    }
405
406
    protected function writeIndexToTemporaryFile(Index $index): string
407
    {
408
        $path = tempnam(sys_get_temp_dir(), 'index');
409
        $stream = fopen($path, 'w');
410
411
        foreach ($index as $object)
412
        {
413
            /** @var IndexObject $object */
414
415
            if (fputcsv($stream, $object->getIndexRecord()) === false)
416
            {
417
                throw new \RuntimeException();
418
            }
419
        }
420
421
        fclose($stream);
422
423
        return $path;
424
    }
425
}
426