Completed
Push — master ( 2b3f4c...5ada1f )
by Raffael
77:32 queued 61:21
created

File   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 629
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 11.31%

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 14
dl 0
loc 629
ccs 31
cts 274
cp 0.1131
rs 2.931
c 0
b 0
f 0

23 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 17 2
A get() 0 12 3
B copyTo() 0 35 7
A getHistory() 0 4 1
C restore() 0 76 9
A delete() 0 30 4
A isTemporaryFile() 0 10 3
A deleteVersion() 0 32 5
A cleanHistory() 0 8 2
A getAttributes() 0 27 1
A getExtension() 0 9 2
A getSize() 0 4 1
A getETag() 0 4 1
A getHash() 0 4 1
A getVersion() 0 4 1
A put() 0 10 1
B setContent() 0 56 10
A copyToCollection() 0 18 2
A _forceDelete() 0 23 2
A increaseVersion() 0 16 2
A prePutFile() 0 14 3
A addVersion() 0 38 3
A postPutFile() 0 30 2

How to fix   Complexity   

Complex Class

Complex classes like File often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use File, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Node;
13
14
use Balloon\Filesystem;
15
use Balloon\Filesystem\Acl;
16
use Balloon\Filesystem\Acl\Exception as AclException;
17
use Balloon\Filesystem\Exception;
18
use Balloon\Filesystem\Storage\Exception as StorageException;
19
use Balloon\Hook;
20
use MongoDB\BSON\ObjectId;
21
use MongoDB\BSON\UTCDateTime;
22
use Psr\Log\LoggerInterface;
23
use Sabre\DAV\IFile;
24
25
class File extends AbstractNode implements IFile
26
{
27
    /**
28
     * History types.
29
     */
30
    const HISTORY_CREATE = 0;
31
    const HISTORY_EDIT = 1;
32
    const HISTORY_RESTORE = 2;
33
34
    /**
35
     * Empty content hash (NULL).
36
     */
37
    const EMPTY_CONTENT = 'd41d8cd98f00b204e9800998ecf8427e';
38
39
    /**
40
     * Temporary file patterns.
41
     *
42
     * @param array
43
     **/
44
    protected $temp_files = [
45
        '/^\._(.*)$/',     // OS/X resource forks
46
        '/^.DS_Store$/',   // OS/X custom folder settings
47
        '/^desktop.ini$/', // Windows custom folder settings
48
        '/^Thumbs.db$/',   // Windows thumbnail cache
49
        '/^.(.*).swpx$/',  // ViM temporary files
50
        '/^.(.*).swx$/',   // ViM temporary files
51
        '/^.(.*).swp$/',   // ViM temporary files
52
        '/^\.dat(.*)$/',   // Smultron seems to create these
53
        '/^~lock.(.*)#$/', // Windows 7 lockfiles
54
    ];
55
56
    /**
57
     * MD5 Hash of the content.
58
     *
59
     * @var string
60
     */
61
    protected $hash;
62
63
    /**
64
     * File version.
65
     *
66
     * @var int
67
     */
68
    protected $version = 0;
69
70
    /**
71
     * History.
72
     *
73
     * @var array
74
     */
75
    protected $history = [];
76
77
    /**
78
     * Initialize file node.
79
     */
80 17
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Collection $parent)
81
    {
82 17
        $this->_fs = $fs;
83 17
        $this->_server = $fs->getServer();
84 17
        $this->_db = $fs->getDatabase();
85 17
        $this->_user = $fs->getUser();
86 17
        $this->_logger = $logger;
87 17
        $this->_hook = $hook;
88 17
        $this->_acl = $acl;
89 17
        $this->_parent = $parent;
90
91 17
        foreach ($attributes as $attr => $value) {
92 17
            $this->{$attr} = $value;
93
        }
94
95 17
        $this->raw_attributes = $attributes;
96 17
    }
97
98
    /**
99
     * Read content and return ressource.
100
     */
101
    public function get()
102
    {
103
        if (null === $this->storage) {
104
            return null;
105
        }
106
107
        try {
108
            return $this->_parent->getStorage()->openReadStream($this);
109
        } catch (\Exception $e) {
110
            throw new Exception\NotFound('storage blob is gone', Exception\NotFound::CONTENTS_NOT_FOUND, $e);
111
        }
112
    }
113
114
    /**
115
     * Copy node.
116
     */
117
    public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true, int $deleted = NodeInterface::DELETED_EXCLUDE): NodeInterface
118
    {
119
        $this->_hook->run(
120
            'preCopyFile',
121
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
122
        );
123
124
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
125
            $name = $this->getDuplicateName();
126
        } else {
127
            $name = $this->name;
128
        }
129
130
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
131
            $result = $parent->getChild($this->name);
132
133
            if ($result instanceof Collection) {
134
                $result = $this->copyToCollection($result, $name);
135
            } else {
136
                $stream = $this->get();
137
                if ($stream !== null) {
138
                    $result->put($stream);
139
                }
140
            }
141
        } else {
142
            $result = $this->copyToCollection($parent, $name);
143
        }
144
145
        $this->_hook->run(
146
            'postCopyFile',
147
            [$this, $parent, $result, $conflict, $recursion, $recursion_first]
148
        );
149
150
        return $result;
151
    }
152
153
    /**
154
     * Get history.
155
     */
156
    public function getHistory(): array
157
    {
158
        return array_values($this->history);
159
    }
160
161
    /**
162
     * Restore content to some older version.
163
     */
164
    public function restore(int $version): bool
165
    {
166
        if (!$this->_acl->isAllowed($this, 'w')) {
167
            throw new AclException\Forbidden('not allowed to restore node '.$this->name, AclException\Forbidden::NOT_ALLOWED_TO_RESTORE);
168
        }
169
170
        $this->_hook->run('preRestoreFile', [$this, &$version]);
171
172
        if ($this->readonly) {
173
            throw new Exception\Conflict('node is marked as readonly, it is not possible to change any content', Exception\Conflict::READONLY);
174
        }
175
176
        if ($this->version === $version) {
177
            throw new Exception('file is already version '.$version);
178
        }
179
180
        $current = $this->version;
0 ignored issues
show
Unused Code introduced by
$current 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...
181
182
        $v = array_search($version, array_column($this->history, 'version'), true);
183
        if (false === $v) {
184
            throw new Exception('failed restore file to version '.$version.', version was not found');
185
        }
186
187
        $file = $this->history[$v]['storage'];
188
        $latest = $this->version + 1;
189
190
        $this->history[] = [
191
            'version' => $latest,
192
            'changed' => $this->changed,
193
            'user' => $this->owner,
194
            'type' => self::HISTORY_RESTORE,
195
            'hash' => $this->history[$v]['hash'],
196
            'origin' => $this->history[$v]['version'],
197
            'storage' => $this->history[$v]['storage'],
198
            'size' => $this->history[$v]['size'],
199
            'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : $this->mime,
200
        ];
201
202
        try {
203
            $this->deleted = false;
204
            $this->storage = $this->history[$v]['storage'];
205
206
            $this->hash = null === $file ? self::EMPTY_CONTENT : $this->history[$v]['hash'];
207
            $this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : $this->mime;
208
            $this->size = $this->history[$v]['size'];
209
            $this->changed = $this->history[$v]['changed'];
210
            $new = $this->increaseVersion();
211
            $this->version = $new;
212
213
            $this->save([
214
                'deleted',
215
                'version',
216
                'storage',
217
                'hash',
218
                'mime',
219
                'size',
220
                'history',
221
                'changed',
222
            ]);
223
224
            $this->_hook->run('postRestoreFile', [$this, &$version]);
225
226
            $this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [
227
                'category' => get_class($this),
228
            ]);
229
        } catch (\Exception $e) {
230
            $this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [
231
                'category' => get_class($this),
232
                'exception' => $e,
233
            ]);
234
235
            throw $e;
236
        }
237
238
        return true;
239
    }
240
241
    /**
242
     * Delete node.
243
     *
244
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
245
     * delete finally
246
     */
247
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
248
    {
249
        if (!$this->_acl->isAllowed($this, 'w')) {
250
            throw new AclException\Forbidden('not allowed to delete node '.$this->name, AclException\Forbidden::NOT_ALLOWED_TO_DELETE);
251
        }
252
253
        $this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]);
254
255
        if (true === $force || $this->isTemporaryFile()) {
256
            $result = $this->_forceDelete();
257
            $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
258
259
            return $result;
260
        }
261
262
        $ts = new UTCDateTime();
263
        $this->deleted = $ts;
264
        $this->storage = $this->_parent->getStorage()->deleteFile($this);
265
266
        $result = $this->save([
267
            'version',
268
            'storage',
269
            'deleted',
270
            'history',
271
        ], [], $recursion, $recursion_first);
272
273
        $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
274
275
        return $result;
276
    }
277
278
    /**
279
     * Check if file is temporary.
280
     */
281 2
    public function isTemporaryFile(): bool
282
    {
283 2
        foreach ($this->temp_files as $pattern) {
284 2
            if (preg_match($pattern, $this->name)) {
285 1
                return true;
286
            }
287
        }
288
289 1
        return false;
290
    }
291
292
    /**
293
     * Delete version.
294
     */
295
    public function deleteVersion(int $version): bool
296
    {
297
        $key = array_search($version, array_column($this->history, 'version'), true);
298
299
        if (false === $key) {
300
            throw new Exception('version '.$version.' does not exists');
301
        }
302
303
        $blobs = array_column($this->history, 'storage');
304
305
        try {
306
            //do not remove blob if there are other versions linked against it
307
            if ($this->history[$key]['storage'] !== null && count(array_keys($blobs, $this->history[$key]['storage'])) === 1) {
308
                $this->_parent->getStorage()->forceDeleteFile($this, $version);
309
            }
310
311
            array_splice($this->history, $key, 1);
312
313
            $this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [
314
                'category' => get_class($this),
315
            ]);
316
317
            return $this->save('history');
318
        } catch (StorageException\BlobNotFound $e) {
319
            $this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [
320
                'category' => get_class($this),
321
                'exception' => $e,
322
            ]);
323
324
            return false;
325
        }
326
    }
327
328
    /**
329
     * Cleanup history.
330
     */
331
    public function cleanHistory(): bool
332
    {
333
        foreach ($this->history as $node) {
334
            $this->deleteVersion($node['version']);
335
        }
336
337
        return true;
338
    }
339
340
    /**
341
     * Get Attributes.
342
     */
343
    public function getAttributes(): array
344
    {
345
        return [
346
            '_id' => $this->_id,
347
            'name' => $this->name,
348
            'hash' => $this->hash,
349
            'directory' => false,
350
            'size' => $this->size,
351
            'version' => $this->version,
352
            'parent' => $this->parent,
353
            'acl' => $this->acl,
354
            'lock' => $this->lock,
355
            'app' => $this->app,
356
            'meta' => $this->meta,
357
            'mime' => $this->mime,
358
            'owner' => $this->owner,
359
            'history' => $this->history,
360
            'shared' => $this->shared,
361
            'deleted' => $this->deleted,
362
            'changed' => $this->changed,
363
            'created' => $this->created,
364
            'destroy' => $this->destroy,
365
            'readonly' => $this->readonly,
366
            'storage_reference' => $this->storage_reference,
367
            'storage' => $this->storage,
368
        ];
369
    }
370
371
    /**
372
     * Get filename extension.
373
     */
374 2
    public function getExtension(): string
375
    {
376 2
        $ext = strrchr($this->name, '.');
377 2
        if (false === $ext) {
378 1
            throw new Exception('file does not have an extension');
379
        }
380
381 1
        return substr($ext, 1);
382
    }
383
384
    /**
385
     * Get file size.
386
     */
387 1
    public function getSize(): int
388
    {
389 1
        return $this->size;
390
    }
391
392
    /**
393
     * Get md5 sum of the file content,
394
     * actually the hash value comes from the database.
395
     */
396 1
    public function getETag(): string
397
    {
398 1
        return '"'.$this->hash.'"';
399
    }
400
401
    /**
402
     * Get hash.
403
     */
404 1
    public function getHash(): ?string
405
    {
406 1
        return $this->hash;
407
    }
408
409
    /**
410
     * Get version.
411
     */
412 1
    public function getVersion(): int
413
    {
414 1
        return $this->version;
415
    }
416
417
    /**
418
     * Change content (Sabe dav compatible method).
419
     */
420
    public function put($content): int
421
    {
422
        $this->_logger->debug('write new file content into temporary storage for file ['.$this->_id.']', [
423
            'category' => get_class($this),
424
        ]);
425
426
        $session = $this->_parent->getStorage()->storeTemporaryFile($content, $this->_user);
427
428
        return $this->setContent($session);
429
    }
430
431
    /**
432
     * Set content (temporary file).
433
     */
434
    public function setContent(ObjectId $session, array $attributes = []): int
435
    {
436
        $this->_logger->debug('set temporary file ['.$session.'] as file content for ['.$this->_id.']', [
437
            'category' => get_class($this),
438
        ]);
439
440
        $previous = $this->version;
441
        $storage = $this->storage;
442
        $this->prePutFile($session);
443
        $result = $this->_parent->getStorage()->storeFile($this, $session);
444
        $this->storage = $result['reference'];
445
446
        if ($this->isDeleted() && $this->hash === $result['hash']) {
447
            $this->deleted = false;
448
            $this->save(['deleted']);
449
        }
450
451
        $this->deleted = false;
452
453
        if ($this->hash === $result['hash']) {
454
            $this->_logger->debug('do not update file version, hash identical to existing version ['.$this->hash.' == '.$result['hash'].']', [
455
                'category' => get_class($this),
456
            ]);
457
458
            return $this->version;
459
        }
460
461
        $this->hash = $result['hash'];
462
        $this->size = $result['size'];
463
464
        if ($this->size === 0 && $this->getMount() === null) {
465
            $this->storage = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $storage.

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...
466
        } else {
467
            $this->storage = $result['reference'];
468
        }
469
470
        $this->increaseVersion();
471
472
        if (isset($attributes['changed'])) {
473
            if (!($attributes['changed'] instanceof UTCDateTime)) {
0 ignored issues
show
Bug introduced by
The class MongoDB\BSON\UTCDateTime does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
474
                throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime');
475
            }
476
477
            $this->changed = $attributes['changed'];
478
        } else {
479
            $this->changed = new UTCDateTime();
480
        }
481
482
        if ($result['reference'] != $storage || $previous === 0) {
483
            $this->addVersion($attributes);
484
        }
485
486
        $this->postPutFile();
487
488
        return $this->version;
489
    }
490
491
    /**
492
     * Copy to collection.
493
     */
494
    protected function copyToCollection(Collection $parent, string $name): NodeInterface
495
    {
496
        $result = $parent->addFile($name, null, [
497
            'created' => $this->created,
498
            'changed' => $this->changed,
499
            'meta' => $this->meta,
500
        ], NodeInterface::CONFLICT_NOACTION, true);
501
502
        $stream = $this->get();
503
504
        if ($stream !== null) {
505
            $session = $parent->getStorage()->storeTemporaryFile($stream, $this->_server->getUserById($this->getOwner()));
506
            $result->setContent($session);
507
            fclose($stream);
508
        }
509
510
        return $result;
511
    }
512
513
    /**
514
     * Completly remove file.
515
     */
516
    protected function _forceDelete(): bool
517
    {
518
        try {
519
            $this->_parent->getStorage()->forceDeleteFile($this);
520
            $this->cleanHistory();
521
            $this->_db->storage->deleteOne([
522
                '_id' => $this->_id,
523
            ]);
524
525
            $this->_logger->info('removed file node ['.$this->_id.']', [
526
                'category' => get_class($this),
527
            ]);
528
        } catch (\Exception $e) {
529
            $this->_logger->error('failed delete file node ['.$this->_id.']', [
530
                'category' => get_class($this),
531
                'exception' => $e,
532
            ]);
533
534
            throw $e;
535
        }
536
537
        return true;
538
    }
539
540
    /**
541
     * Increase version.
542
     */
543
    protected function increaseVersion(): int
544
    {
545
        $max = $this->_fs->getServer()->getMaxFileVersion();
546
        if (count($this->history) >= $max) {
547
            $del = key($this->history);
548
            $this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$this->history[$del]['version'].'] from file ['.$this->_id.']', [
549
                'category' => get_class($this),
550
            ]);
551
552
            $this->deleteVersion($this->history[$del]['version']);
553
        }
554
555
        ++$this->version;
556
557
        return $this->version;
558
    }
559
560
    /**
561
     * Pre content change checks.
562
     */
563
    protected function prePutFile(ObjectId $session): bool
564
    {
565
        if (!$this->_acl->isAllowed($this, 'w')) {
566
            throw new AclException\Forbidden('not allowed to modify node', AclException\Forbidden::NOT_ALLOWED_TO_MODIFY);
567
        }
568
569
        $this->_hook->run('prePutFile', [$this, &$session]);
570
571
        if ($this->readonly) {
572
            throw new Exception\Conflict('node is marked as readonly, it is not possible to change any content', Exception\Conflict::READONLY);
573
        }
574
575
        return true;
576
    }
577
578
    /**
579
     * Add new version.
580
     */
581
    protected function addVersion(array $attributes = []): self
582
    {
583
        if (1 !== $this->version) {
584
            $this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [
585
                'category' => get_class($this),
586
            ]);
587
588
            $this->history[] = [
589
                'version' => $this->version,
590
                'changed' => $this->changed,
591
                'user' => $this->_user->getId(),
592
                'type' => self::HISTORY_EDIT,
593
                'storage' => $this->storage,
594
                'size' => $this->size,
595
                'mime' => $this->mime,
596
                'hash' => $this->hash,
597
            ];
598
599
            return $this;
600
        }
601
602
        $this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [
603
            'category' => get_class($this),
604
        ]);
605
606
        $this->history[0] = [
607
            'version' => 1,
608
            'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(),
609
            'user' => $this->owner,
610
            'type' => self::HISTORY_CREATE,
611
            'storage' => $this->storage,
612
            'size' => $this->size,
613
            'mime' => $this->mime,
614
            'hash' => $this->hash,
615
        ];
616
617
        return $this;
618
    }
619
620
    /**
621
     * Finalize put request.
622
     */
623
    protected function postPutFile(): self
624
    {
625
        try {
626
            $this->save([
627
                'size',
628
                'changed',
629
                'deleted',
630
                'mime',
631
                'hash',
632
                'version',
633
                'history',
634
                'storage',
635
            ]);
636
637
            $this->_logger->debug('modifed file metadata ['.$this->_id.']', [
638
                'category' => get_class($this),
639
            ]);
640
641
            $this->_hook->run('postPutFile', [$this]);
642
643
            return $this;
644
        } catch (\Exception $e) {
645
            $this->_logger->error('failed modify file metadata ['.$this->_id.']', [
646
                'category' => get_class($this),
647
                'exception' => $e,
648
            ]);
649
650
            throw $e;
651
        }
652
    }
653
}
654