Completed
Branch dev (bc6e47)
by Raffael
02:23
created

Delta::getLastCursor()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 22
rs 9.2
c 0
b 0
f 0
cc 3
eloc 12
nc 4
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Balloon
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
10
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
11
 */
12
13
namespace Balloon\Filesystem;
14
15
use Balloon\Filesystem;
16
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
17
use Balloon\Filesystem\Delta\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Balloon\Filesystem\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
18
use Balloon\Filesystem\Node\AttributeDecorator;
19
use Balloon\Filesystem\Node\NodeInterface;
20
use Balloon\Helper;
21
use Balloon\Server\User;
22
use MongoDB\BSON\ObjectId;
23
use MongoDB\BSON\UTCDateTime;
24
use MongoDB\Database;
25
26
class Delta
27
{
28
    /**
29
     * Filesystem.
30
     *
31
     * @var Filesystem
32
     */
33
    protected $fs;
34
35
    /**
36
     * Db.
37
     *
38
     * @var Database
39
     */
40
    protected $db;
41
42
    /**
43
     * User.
44
     *
45
     * @var User
46
     */
47
    protected $user;
48
49
    /**
50
     * Decorator.
51
     *
52
     * @var AttributeDecorator
53
     */
54
    protected $decorator;
55
56
    /**
57
     * Initialize delta.
58
     *
59
     * @param Filesystem $fs
60
     */
61
    public function __construct(Filesystem $fs, Database $db, AttributeDecorator $decorator)
62
    {
63
        $this->fs = $fs;
64
        $this->db = $db;
65
        $this->user = $fs->getUser();
66
        $this->decorator = $decorator;
67
    }
68
69
    /**
70
     * Add delta event.
71
     *
72
     * @param array $options
73
     *
74
     * @return bool
75
     */
76
    public function add(array $options): bool
77
    {
78
        if (!$this->isValidDeltaEvent($options)) {
79
            throw new Exception('invalid delta structure given');
80
        }
81
82
        if (!array_key_exists('timestamp', $options)) {
83
            $options['timestamp'] = new UTCDateTime();
84
        }
85
86
        $result = $this->db->delta->insertOne($options);
87
88
        return $result->isAcknowledged();
89
    }
90
91
    /**
92
     * Build a single dimension array with all nodes.
93
     *
94
     * @param array         $cursor
95
     * @param int           $limit
96
     * @param array         $attributes
97
     * @param NodeInterface $node
98
     *
99
     * @return array
100
     */
101
    public function buildFeedFromCurrentState(?array $cursor = null, int $limit = 100, array $attributes = [], ?NodeInterface $node = null): array
102
    {
103
        $current_cursor = 0;
104
        $filter = ['$and' => [
105
            ['$or' => [
106
                ['shared' => [
107
                    '$in' => $this->user->getShares(),
108
                ]],
109
                [
110
                    'shared' => ['$type' => 8],
111
                    'owner' => $this->user->getId(),
112
                ],
113
            ]],
114
            ['deleted' => false],
115
        ]];
116
117
        if (is_array($cursor)) {
118
            $current_cursor = $cursor[1];
119
        }
120
121
        $children = $this->findNodeAttributesWithCustomFilter(
122
            $filter,
123
            $attributes,
124
            $limit,
125
            $current_cursor,
126
            $has_more,
127
            $node
128
        );
129
130
        $reset = false;
131
132
        if (null === $cursor) {
133
            $last = $this->getLastRecord();
134
            if (null === $last) {
135
                $delta_id = 0;
136
                $ts = new UTCDateTime();
137
            } else {
138
                $delta_id = $last['_id'];
139
                $ts = $last['timestamp'];
140
            }
141
142
            $reset = true;
143
            if (false === $has_more) {
144
                $cursor = base64_encode('delta|0|0|'.$delta_id.'|'.$ts);
145
            } else {
146
                $cursor = base64_encode('initial|'.$current_cursor.'|'.end($children)['id'].'|'.$delta_id.'|'.$ts);
147
            }
148
        } else {
149
            if (false === $has_more) {
150
                $cursor = base64_encode('delta|0|0|'.$cursor[3].'|'.$cursor[4]);
151
            } else {
152
                $cursor = base64_encode('initial|'.$current_cursor.'|'.end($children)['id'].'|'.$cursor[3].'|'.$cursor[4]);
153
            }
154
        }
155
156
        return [
157
            'reset' => $reset,
158
            'cursor' => $cursor,
159
            'has_more' => $has_more,
160
            'nodes' => $children,
161
        ];
162
    }
163
164
    /**
165
     * Get last delta event.
166
     *
167
     * @param NodeInterface $node
168
     *
169
     * @return array
170
     */
171
    public function getLastRecord(?NodeInterface $node = null): ?array
172
    {
173
        $filter = $this->getDeltaFilter();
174
175
        if (null !== $node) {
176
            $filter = [
177
                '$and' => [
178
                    ['node' => $node->getId()],
179
                    $filter,
180
                ],
181
            ];
182
        }
183
184
        $cursor = $this->db->delta->find($filter, [
185
            'sort' => ['timestamp' => -1],
186
            'limit' => 1,
187
        ]);
188
189
        $last = $cursor->toArray();
190
191
        return array_shift($last);
192
    }
193
194
    /**
195
     * Get last cursor.
196
     *
197
     * @param NodeInterface $node
198
     *
199
     * @return string
200
     */
201
    public function getLastCursor(?NodeInterface $node = null): string
202
    {
203
        $filter = $this->getDeltaFilter();
204
205
        if (null !== $node) {
206
            $filter = [
207
                '$and' => [
208
                    ['node' => $node->getId()],
209
                    $filter,
210
                ],
211
            ];
212
        }
213
214
        $count = $this->db->delta->count($filter);
0 ignored issues
show
Unused Code introduced by
$count 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...
215
        $last = $this->getLastRecord($node);
216
217
        if (null === $last) {
218
            return base64_encode('delta|0|0|0|'.new UTCDateTime());
219
        }
220
221
        return $cursor = base64_encode('delta|0|0|'.$last['_id'].'|'.$last['timestamp']);
0 ignored issues
show
Unused Code introduced by
$cursor 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...
222
    }
223
224
    /**
225
     * Get delta feed with changes and cursor.
226
     *
227
     * @param string        $cursor
228
     * @param int           $limit
229
     * @param array         $attributes
230
     * @param NodeInterface $node
231
     *
232
     * @return array
233
     */
234
    public function getDeltaFeed(?string $cursor = null, int $limit = 250, array $attributes = [], ?NodeInterface $node = null): array
235
    {
236
        $this->user->findNewShares();
237
238
        $attributes = array_merge(
239
            ['id', 'directory', 'deleted',  'path', 'changed', 'created', 'owner'],
240
            $attributes
241
        );
242
243
        $cursor = $this->decodeCursor($cursor);
244
245
        if (null === $cursor || 'initial' === $cursor[0]) {
246
            return $this->buildFeedFromCurrentState($cursor, $limit, $attributes, $node);
247
        }
248
249
        try {
250
            if (0 === $cursor[3]) {
251
                $filter = $this->getDeltaFilter();
252
            } else {
253
                //check if delta entry actually exists
254
                if (0 === $this->db->delta->count(['_id' => new ObjectId($cursor[3])])) {
255
                    return $this->buildFeedFromCurrentState(null, $limit, $attributes, $node);
256
                }
257
258
                $filter = $this->getDeltaFilter();
259
                $filter = [
260
                    '$and' => [
261
                        ['timestamp' => ['$gte' => new UTCDateTime($cursor[4])]],
262
                        ['_id' => ['$gt' => new ObjectId($cursor[3])]],
263
                        $filter,
264
                    ],
265
                ];
266
            }
267
268
            $result = $this->db->delta->find($filter, [
269
                'skip' => (int) $cursor[1],
270
                'limit' => (int) $limit,
271
                'sort' => ['timestamp' => 1],
272
            ]);
273
274
            $left = $this->db->delta->count($filter, [
275
                'skip' => (int) $cursor[1],
276
                'sort' => ['timestamp' => 1],
277
            ]);
278
279
            $result = $result->toArray();
280
            $count = count($result);
281
            $list = [];
282
            $last_id = $cursor[3];
283
            $last_ts = $cursor[4];
284
        } catch (\Exception $e) {
285
            return $this->buildFeedFromCurrentState(null, $limit, $attributes, $node);
286
        }
287
288
        $cursor = $cursor[1] += $limit;
289
        $has_more = ($left - $count) > 0;
290
        if (false === $has_more) {
291
            $cursor = 0;
292
        }
293
294
        foreach ($result as $log) {
295
            if (false === $has_more) {
296
                $last_id = (string) $log['_id'];
297
                $last_ts = (string) $log['timestamp'];
298
            }
299
300
            try {
301
                $log_node = $this->fs->findNodeById($log['node'], null, NodeInterface::DELETED_EXCLUDE);
302
                if (null !== $node && !$node->isSubNode($log_node)) {
303
                    continue;
304
                }
305
306
                //include share children after a new reference was added, otherwise the children would be lost if the cursor is newer
307
                //than the create timestamp of the share reference
308
                if ('addCollectionReference' === $log['operation'] && $log_node->isReference()) {
309
                    $members = $this->fs->findNodesByFilter([
310
                        'shared' => $log_node->getShareId(),
311
                        'deleted' => false,
312
                    ]);
313
314
                    foreach ($members as $share_member) {
315
                        $member_attrs = $this->decorator->decorate($share_member, $attributes);
316
                        $list[$member_attrs['path']] = $member_attrs;
317
                    }
318
                }
319
320
                $fields = $this->decorator->decorate($log_node, $attributes);
321
322
                if (array_key_exists('previous', $log)) {
323
                    if (array_key_exists('parent', $log['previous'])) {
324
                        if ($log['previous']['parent'] === null) {
325
                            $previous_path = DIRECTORY_SEPARATOR.$log['name'];
326
                        } else {
327
                            $parent = $this->fs->findNodeById($log['previous']['parent']);
328
                            $previous_path = $parent->getPath().DIRECTORY_SEPARATOR.$log['name'];
329
                        }
330
                    } elseif (array_key_exists('name', $log['previous'])) {
331
                        if (null === $log['parent']) {
332
                            $previous_path = DIRECTORY_SEPARATOR.$log['previous']['name'];
333
                        } else {
334
                            $parent = $this->fs->findNodeById($log['parent']);
335
                            $previous_path = $parent->getPath().DIRECTORY_SEPARATOR.$log['previous']['name'];
336
                        }
337
                    } else {
338
                        $list[$fields['path']] = $fields;
339
340
                        continue;
341
                    }
342
343
                    $deleted_node = [
344
                        'id' => (string) $log['node'],
345
                        'deleted' => true,
346
                        'created' => null,
347
                        'changed' => Helper::DateTimeToUnix($log['timestamp']),
348
                        'path' => $previous_path,
349
                        'directory' => $fields['directory'],
350
                    ];
351
352
                    $list[$previous_path] = $deleted_node;
353
                    $list[$fields['path']] = $fields;
354
                } else {
355
                    $list[$fields['path']] = $fields;
356
                }
357
            } catch (ForbiddenException $e) {
358
                //no delta entriy for a node where we do not have access to
359
            } catch (\Exception $e) {
360
                try {
361
                    if (null === $log['parent']) {
362
                        $path = DIRECTORY_SEPARATOR.$log['name'];
363
                    } else {
364
                        $parent = $this->fs->findNodeById($log['parent']);
365
                        $path = $parent->getPath().DIRECTORY_SEPARATOR.$log['name'];
366
                    }
367
368
                    $entry = [
369
                        'id' => (string) $log['node'],
370
                        'deleted' => true,
371
                        'created' => null,
372
                        'changed' => Helper::DateTimeToUnix($log['timestamp']),
373
                        'path' => $path,
374
                    ];
375
376
                    if ('deleteCollection' === substr($log['operation'], 0, 16)) {
377
                        $entry['directory'] = true;
378
                    } elseif ('deleteFile' === substr($log['operation'], 0, 10)) {
379
                        $entry['directory'] = false;
380
                    }
381
382
                    $list[$path] = $entry;
383
                } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
384
                }
385
            }
386
        }
387
388
        $cursor = base64_encode('delta|'.$cursor.'|0|'.$last_id.'|'.$last_ts);
389
390
        return [
391
            'reset' => false,
392
            'cursor' => $cursor,
393
            'has_more' => $has_more,
394
            'nodes' => array_values($list),
395
        ];
396
    }
397
398
    /**
399
     * Get event log.
400
     *
401
     * @param int           $limit
402
     * @param int           $skip
403
     * @param NodeInterface $node
404
     *
405
     * @return array
406
     */
407
    public function getEventLog(int $limit = 100, int $skip = 0, ?NodeInterface $node = null): array
408
    {
409
        $filter = $this->getDeltaFilter();
410
411
        if (null !== $node) {
412
            $old = $filter;
413
            $filter = ['$and' => [[
414
                'node' => $node->getId(),
415
            ],
416
            $old, ]];
417
        }
418
419
        $result = $this->db->delta->find($filter, [
420
            'sort' => ['_id' => -1],
421
            'skip' => $skip,
422
            'limit' => $limit,
423
        ]);
424
425
        $client = [
426
            'type' => null,
427
            'app' => null,
428
            'v' => null,
429
            'hostname' => null,
430
        ];
431
432
        $events = [];
433
        foreach ($result as $log) {
434
            $id = (string) $log['_id'];
435
            $events[$id] = [
436
                'event' => $id,
437
                'timestamp' => Helper::DateTimeToUnix($log['timestamp']),
438
                'operation' => $log['operation'],
439
                'name' => $log['name'],
440
                'client' => isset($log['client']) ? $log['client'] : $client,
441
            ];
442
443
            if (isset($log['previous'])) {
444
                $events[$id]['previous'] = $log['previous'];
445
446
                if (array_key_exists('parent', $events[$id]['previous'])) {
447
                    if ($events[$id]['previous']['parent'] === null) {
448
                        $events[$id]['previous']['parent'] = [
449
                            'id' => null,
450
                            'name' => null,
451
                        ];
452
                    } else {
453
                        try {
454
                            $node = $this->fs->findNodeById($events[$id]['previous']['parent'], null, NodeInterface::DELETED_INCLUDE);
455
                            $events[$id]['previous']['parent'] = [
456
                                'id' => (string) $node->getId(),
457
                                'name' => $node->getName(),
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
458
                            ];
459
                        } catch (\Exception $e) {
460
                            $events[$id]['previous']['parent'] = null;
461
                        }
462
                    }
463
                }
464
            } else {
465
                $events[$id]['previous'] = null;
466
            }
467
468
            try {
469
                $node = $this->fs->findNodeById($log['node'], null, NodeInterface::DELETED_INCLUDE);
470
                $events[$id]['node'] = [
471
                    'id' => (string) $node->getId(),
472
                    'name' => $node->getName(),
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
473
                    'deleted' => $node->isDeleted(),
474
                ];
475
            } catch (\Exception $e) {
476
                $events[$id]['node'] = null;
477
            }
478
479
            try {
480
                if (null === $log['parent']) {
481
                    $events[$id]['parent'] = [
482
                        'id' => null,
483
                        'name' => null,
484
                    ];
485
                } else {
486
                    $node = $this->fs->findNodeById($log['parent'], null, NodeInterface::DELETED_INCLUDE);
487
                    $events[$id]['parent'] = [
488
                        'id' => (string) $node->getId(),
489
                        'name' => $node->getName(),
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
490
                        'deleted' => $node->isDeleted(),
491
                    ];
492
                }
493
            } catch (\Exception $e) {
494
                $events[$id]['parent'] = null;
495
            }
496
497
            try {
498
                $user = $this->fs->getServer()->getUserById($log['owner']);
499
                $events[$id]['user'] = [
500
                    'id' => (string) $user->getId(),
501
                    'username' => $user->getUsername(),
502
                ];
503
            } catch (\Exception $e) {
504
                $events[$id]['user'] = null;
505
            }
506
507
            try {
508
                if (isset($log['share']) && false === $log['share'] || !isset($log['share'])) {
509
                    $events[$id]['share'] = null;
510
                } else {
511
                    $node = $this->fs->findNodeById($log['share'], null, NodeInterface::DELETED_INCLUDE);
512
                    $events[$id]['share'] = [
513
                        'id' => (string) $node->getId(),
514
                        'name' => $node->getName(),
0 ignored issues
show
Bug introduced by
Consider using $node->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
515
                        'deleted' => $node->isDeleted(),
516
                    ];
517
                }
518
            } catch (\Exception $e) {
519
                $events[$id]['share'] = null;
520
            }
521
        }
522
523
        return array_values($events);
524
    }
525
526
    /**
527
     * Verify delta structure.
528
     *
529
     * @param array $options
530
     *
531
     * @return bool
532
     */
533
    protected function isValidDeltaEvent(array $options): bool
534
    {
535
        if (!array_key_exists('operation', $options)) {
536
            return false;
537
        }
538
539
        return array_key_exists('owner', $options) || array_key_exists('share', $options);
540
    }
541
542
    /**
543
     * Decode cursor.
544
     *
545
     * @param string $cursor
546
     *
547
     * @return array
548
     */
549
    protected function decodeCursor(?string $cursor): ?array
550
    {
551
        if (null === $cursor) {
552
            return null;
553
        }
554
555
        $cursor = base64_decode($cursor, true);
556
        if (false === $cursor) {
557
            return null;
558
        }
559
560
        $cursor = explode('|', $cursor);
561
        if (5 !== count($cursor)) {
562
            return null;
563
        }
564
        $cursor[1] = (int) $cursor[1];
565
566
        return $cursor;
567
    }
568
569
    /**
570
     * Get delta filter for db query.
571
     *
572
     * @return array
573
     */
574
    protected function getDeltaFilter(): array
575
    {
576
        return [
577
            '$or' => [
578
                ['share' => [
579
                    '$in' => $this->user->getShares(),
580
                ]], [
581
                    'owner' => $this->user->getId(),
582
                ],
583
            ],
584
        ];
585
    }
586
587
    /**
588
     * Get children with custom filter.
589
     *
590
     * @param array         $filter
591
     * @param array         $attributes
592
     * @param int           $limit
593
     * @param string        $cursor
594
     * @param bool          $has_more
595
     * @param NodeInterface $parent
596
     *
597
     * @return array
598
     */
599
    protected function findNodeAttributesWithCustomFilter(
600
        ?array $filter = null,
601
        array $attributes = ['_id'],
602
        ?int $limit = null,
603
        ?int &$cursor = null,
604
        ?bool &$has_more = null,
605
        ?NodeInterface $parent = null
606
    ) {
607
        $list = [];
608
609
        $result = $this->db->storage->find($filter, [
610
            'skip' => $cursor,
611
            'limit' => $limit,
612
        ]);
613
614
        $left = $this->db->storage->count($filter, [
615
            'skip' => $cursor,
616
        ]);
617
618
        $result = $result->toArray();
619
        $count = count($result);
620
        $has_more = ($left - $count) > 0;
621
622
        foreach ($result as $node) {
623
            ++$cursor;
624
625
            try {
626
                $node = $this->fs->initNode($node);
627
628
                if (null !== $parent && !$parent->isSubNode($node)) {
629
                    continue;
630
                }
631
            } catch (\Exception $e) {
632
                continue;
633
            }
634
635
            $list[] = $this->decorator->decorate($node, $attributes);
636
        }
637
638
        $has_more = ($left - $count) > 0;
639
640
        return $list;
641
    }
642
}
643