Completed
Push — master ( 37faaa...541bbf )
by Raffael
10:18 queued 06:30
created

File::setContent()   C

Complexity

Conditions 10
Paths 22

Size

Total Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 0
Metric Value
dl 0
loc 62
ccs 0
cts 33
cp 0
rs 6.9624
c 0
b 0
f 0
cc 10
crap 110
nc 22
nop 2

How to fix   Long Method    Complexity   

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
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 Balloon\Session\Factory as SessionFactory;
21
use Balloon\Session\SessionInterface;
22
use MongoDB\BSON\ObjectId;
23
use MongoDB\BSON\UTCDateTime;
24
use Psr\Log\LoggerInterface;
25
use Sabre\DAV\IFile;
26
27
class File extends AbstractNode implements IFile
28
{
29
    /**
30
     * History types.
31
     */
32
    const HISTORY_CREATE = 0;
33
    const HISTORY_EDIT = 1;
34
    const HISTORY_RESTORE = 2;
35
36
    /**
37
     * Empty content hash (NULL).
38
     */
39
    const EMPTY_CONTENT = 'd41d8cd98f00b204e9800998ecf8427e';
40
41
    /**
42
     * Temporary file patterns.
43
     *
44
     * @param array
45
     **/
46
    protected $temp_files = [
47
        '/^\._(.*)$/',     // OS/X resource forks
48
        '/^.DS_Store$/',   // OS/X custom folder settings
49
        '/^desktop.ini$/', // Windows custom folder settings
50
        '/^Thumbs.db$/',   // Windows thumbnail cache
51
        '/^.(.*).swpx$/',  // ViM temporary files
52
        '/^.(.*).swx$/',   // ViM temporary files
53
        '/^.(.*).swp$/',   // ViM temporary files
54
        '/^\.dat(.*)$/',   // Smultron seems to create these
55
        '/^~lock.(.*)#$/', // Windows 7 lockfiles
56
        '/^\~\$/',         // Temporary office files
57
    ];
58
59
    /**
60
     * MD5 Hash of the content.
61
     *
62
     * @var string
63
     */
64
    protected $hash;
65
66
    /**
67
     * File version.
68
     *
69
     * @var int
70
     */
71
    protected $version = 0;
72
73
    /**
74
     * History.
75
     *
76
     * @var array
77
     */
78
    protected $history = [];
79
80 17
    /**
81
     * Session factory.
82 17
     *
83 17
     * @var SessionFactory
84 17
     */
85 17
    protected $_session_factory;
86 17
87 17
    /**
88 17
     * Initialize file node.
89 17
     */
90
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Collection $parent, SessionFactory $session_factory)
91 17
    {
92 17
        $this->_fs = $fs;
93
        $this->_server = $fs->getServer();
94
        $this->_db = $fs->getDatabase();
95 17
        $this->_user = $fs->getUser();
96 17
        $this->_logger = $logger;
97
        $this->_hook = $hook;
98
        $this->_acl = $acl;
99
        $this->_parent = $parent;
100
        $this->_session_factory = $session_factory;
101
102
        foreach ($attributes as $attr => $value) {
103
            $this->{$attr} = $value;
104
        }
105
106
        $this->raw_attributes = $attributes;
107
    }
108
109
    /**
110
     * Read content and return ressource.
111
     */
112
    public function get()
113
    {
114
        if (null === $this->storage) {
115
            return null;
116
        }
117
118
        try {
119
            return $this->_parent->getStorage()->openReadStream($this);
120
        } catch (\Exception $e) {
121
            throw new Exception\NotFound('storage blob is gone', Exception\NotFound::CONTENTS_NOT_FOUND, $e);
122
        }
123
    }
124
125
    /**
126
     * Copy node.
127
     */
128
    public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true, int $deleted = NodeInterface::DELETED_EXCLUDE): NodeInterface
129
    {
130
        $this->_hook->run(
131
            'preCopyFile',
132
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
133
        );
134
135
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
136
            $name = $this->getDuplicateName();
137
        } else {
138
            $name = $this->name;
139
        }
140
141
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
142
            $result = $parent->getChild($this->name);
143
144
            if ($result instanceof Collection) {
145
                $result = $this->copyToCollection($result, $name);
146
            } else {
147
                $stream = $this->get();
148
                if ($stream !== null) {
149
                    $result->put($stream);
150
                }
151
            }
152
        } else {
153
            $result = $this->copyToCollection($parent, $name);
154
        }
155
156
        $this->_hook->run(
157
            'postCopyFile',
158
            [$this, $parent, $result, $conflict, $recursion, $recursion_first]
159
        );
160
161
        return $result;
162
    }
163
164
    /**
165
     * Get history.
166
     */
167
    public function getHistory(): array
168
    {
169
        return array_values($this->history);
170
    }
171
172
    /**
173
     * Restore content to some older version.
174
     */
175
    public function restore(int $version): bool
176
    {
177
        if (!$this->_acl->isAllowed($this, 'w')) {
178
            throw new AclException\Forbidden('not allowed to restore node '.$this->name, AclException\Forbidden::NOT_ALLOWED_TO_RESTORE);
179
        }
180
181
        $this->_hook->run('preRestoreFile', [$this, &$version]);
182
183
        if ($this->readonly) {
184
            throw new Exception\Conflict('node is marked as readonly, it is not possible to change any content', Exception\Conflict::READONLY);
185
        }
186
187
        if ($this->version === $version) {
188
            throw new Exception('file is already version '.$version);
189
        }
190
191
        $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...
192
193
        $v = array_search($version, array_column($this->history, 'version'), true);
194
        if (false === $v) {
195
            throw new Exception('failed restore file to version '.$version.', version was not found');
196
        }
197
198
        $file = $this->history[$v]['storage'];
199
        $latest = $this->version + 1;
200
201
        $this->history[] = [
202
            'version' => $latest,
203
            'changed' => $this->changed,
204
            'user' => $this->owner,
205
            'type' => self::HISTORY_RESTORE,
206
            'hash' => $this->history[$v]['hash'],
207
            'origin' => $this->history[$v]['version'],
208
            'storage' => $this->history[$v]['storage'],
209
            'size' => $this->history[$v]['size'],
210
            'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : $this->mime,
211
        ];
212
213
        try {
214
            $this->deleted = false;
215
            $this->storage = $this->history[$v]['storage'];
216
217
            $this->hash = null === $file ? self::EMPTY_CONTENT : $this->history[$v]['hash'];
218
            $this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : $this->mime;
219
            $this->size = $this->history[$v]['size'];
220
            $this->changed = $this->history[$v]['changed'];
221
            $new = $this->increaseVersion();
222
            $this->version = $new;
223
224
            $this->save([
225
                'deleted',
226
                'version',
227
                'storage',
228
                'hash',
229
                'mime',
230
                'size',
231
                'history',
232
                'changed',
233
            ]);
234
235
            $this->_hook->run('postRestoreFile', [$this, &$version]);
236
237
            $this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [
238
                'category' => get_class($this),
239
            ]);
240
        } catch (\Exception $e) {
241
            $this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [
242
                'category' => get_class($this),
243
                'exception' => $e,
244
            ]);
245
246
            throw $e;
247
        }
248
249
        return true;
250
    }
251
252
    /**
253
     * Delete node.
254
     *
255
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
256
     * delete finally
257
     */
258
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
259
    {
260
        if (!$this->_acl->isAllowed($this, 'w')) {
261
            throw new AclException\Forbidden('not allowed to delete node '.$this->name, AclException\Forbidden::NOT_ALLOWED_TO_DELETE);
262
        }
263
264
        $this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]);
265
266
        if (true === $force || $this->isTemporaryFile()) {
267
            $result = $this->_forceDelete();
268
            $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
269
270
            return $result;
271
        }
272
273
        $ts = new UTCDateTime();
274
        $this->deleted = $ts;
275
        $this->storage = $this->_parent->getStorage()->deleteFile($this);
276
277
        $result = $this->save([
278
            'version',
279
            'storage',
280
            'deleted',
281 2
            'history',
282
        ], [], $recursion, $recursion_first);
283 2
284 2
        $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
285 1
286
        return $result;
287
    }
288
289 1
    /**
290
     * Check if file is temporary.
291
     */
292
    public function isTemporaryFile(): bool
293
    {
294
        foreach ($this->temp_files as $pattern) {
295
            if (preg_match($pattern, $this->name)) {
296
                return true;
297
            }
298
        }
299
300
        return false;
301
    }
302
303
    /**
304
     * Delete version.
305
     */
306
    public function deleteVersion(int $version): bool
307
    {
308
        $key = array_search($version, array_column($this->history, 'version'), true);
309
310
        if (false === $key) {
311
            throw new Exception('version '.$version.' does not exists');
312
        }
313
314
        $blobs = array_column($this->history, 'storage');
315
316
        try {
317
            //do not remove blob if there are other versions linked against it
318
            if ($this->history[$key]['storage'] !== null && count(array_keys($blobs, $this->history[$key]['storage'])) === 1) {
319
                $this->_parent->getStorage()->forceDeleteFile($this, $version);
320
            }
321
322
            array_splice($this->history, $key, 1);
323
324
            $this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [
325
                'category' => get_class($this),
326
            ]);
327
328
            return $this->save('history');
329
        } catch (StorageException\BlobNotFound $e) {
330
            $this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [
331
                'category' => get_class($this),
332
                'exception' => $e,
333
            ]);
334
335
            return false;
336
        }
337
    }
338
339
    /**
340
     * Cleanup history.
341
     */
342
    public function cleanHistory(): bool
343
    {
344
        foreach ($this->history as $node) {
345
            $this->deleteVersion($node['version']);
346
        }
347
348
        return true;
349
    }
350
351
    /**
352
     * Get Attributes.
353
     */
354
    public function getAttributes(): array
355
    {
356
        return [
357
            '_id' => $this->_id,
358
            'name' => $this->name,
359
            'hash' => $this->hash,
360
            'directory' => false,
361
            'size' => $this->size,
362
            'version' => $this->version,
363
            'parent' => $this->parent,
364
            'acl' => $this->acl,
365
            'lock' => $this->lock,
366
            'app' => $this->app,
367
            'meta' => $this->meta,
368
            'mime' => $this->mime,
369
            'owner' => $this->owner,
370
            'history' => $this->history,
371
            'shared' => $this->shared,
372
            'deleted' => $this->deleted,
373
            'changed' => $this->changed,
374 2
            'created' => $this->created,
375
            'destroy' => $this->destroy,
376 2
            'readonly' => $this->readonly,
377 2
            'storage_reference' => $this->storage_reference,
378 1
            'storage' => $this->storage,
379
        ];
380
    }
381 1
382
    /**
383
     * Get filename extension.
384
     */
385
    public function getExtension(): string
386
    {
387 1
        $ext = strrchr($this->name, '.');
388
        if (false === $ext) {
389 1
            throw new Exception('file does not have an extension');
390
        }
391
392
        return substr($ext, 1);
393
    }
394
395
    /**
396 1
     * Get file size.
397
     */
398 1
    public function getSize(): int
399
    {
400
        return $this->size;
401
    }
402
403
    /**
404 1
     * Get md5 sum of the file content,
405
     * actually the hash value comes from the database.
406 1
     */
407
    public function getETag(): string
408
    {
409
        return '"'.$this->hash.'"';
410
    }
411
412 1
    /**
413
     * Get hash.
414 1
     */
415
    public function getHash(): ?string
416
    {
417
        return $this->hash;
418
    }
419
420
    /**
421
     * Get version.
422
     */
423
    public function getVersion(): int
424
    {
425
        return $this->version;
426
    }
427
428
    /**
429
     * Change content (Sabe dav compatible method).
430
     */
431
    public function put($content): int
432
    {
433
        $this->_logger->debug('write new file content into temporary storage for file ['.$this->_id.']', [
434
            'category' => get_class($this),
435
        ]);
436
437
        $session = $this->_session_factory->add($this->_user, $this->getParent(), $content);
438
439
        return $this->setContent($session);
440
    }
441
442
    /**
443
     * Set content (temporary file).
444
     */
445
    public function setContent(SessionInterface $session, array $attributes = []): int
446
    {
447
        $this->_logger->debug('set temporary file ['.$session->getId().'] as file content for ['.$this->_id.']', [
448
            'category' => get_class($this),
449
        ]);
450
451
        $previous = $this->version;
452
        $storage = $this->storage;
453
        $this->prePutFile($session->getId());
454
455
        $result = $this->_parent->getStorage()->storeFile($this, $session);
456
        $this->storage = $result['reference'];
457
458
        $hash = $session->getHash();
459
460
        if ($this->isDeleted() && $this->hash === $hash) {
461
            $this->deleted = false;
462
            $this->save(['deleted']);
463
        }
464
465
        $this->deleted = false;
466
467
        if ($this->hash === $hash) {
468
            $this->_logger->debug('do not add history entry, hash identical to existing version ['.$this->hash.' == '.$hash.']', [
469
                'category' => get_class($this),
470
            ]);
471
472
            ++$this->version;
473
            $this->save(['version']);
474
475
            return $this->version;
476
        }
477
478
        $this->hash = $hash;
479
        $this->size = $session->getSize();
480
481
        if ($this->size === 0 && $this->getMount() === null) {
482
            $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...
483
        } else {
484
            $this->storage = $result['reference'];
485
        }
486
487
        $this->increaseVersion();
488
489
        if (isset($attributes['changed'])) {
490
            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...
491
                throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime');
492
            }
493
494
            $this->changed = $attributes['changed'];
495
        } else {
496
            $this->changed = new UTCDateTime();
497
        }
498
499
        if ($result['reference'] != $storage || $previous === 0) {
500
            $this->addVersion($attributes);
501
        }
502
503
        $this->postPutFile($session);
504
505
        return $this->version;
506
    }
507
508
    /**
509
     * Copy to collection.
510
     */
511
    protected function copyToCollection(Collection $parent, string $name): NodeInterface
512
    {
513
        $result = $parent->addFile($name, null, [
514
            'created' => $this->created,
515
            'changed' => $this->changed,
516
            'meta' => $this->meta,
517
        ], NodeInterface::CONFLICT_NOACTION, true);
518
519
        $stream = $this->get();
520
521
        if ($stream !== null) {
522
            $session = $this->_session_factory->add($this->_server->getUserById($this->getOwner()), $parent, $stream);
523
            $result->setContent($session);
524
            fclose($stream);
525
        }
526
527
        return $result;
528
    }
529
530
    /**
531
     * Completly remove file.
532
     */
533
    protected function _forceDelete(): bool
534
    {
535
        try {
536
            $this->_parent->getStorage()->forceDeleteFile($this);
537
            $this->cleanHistory();
538
            $this->_db->storage->deleteOne([
539
                '_id' => $this->_id,
540
            ]);
541
542
            $this->_logger->info('removed file node ['.$this->_id.']', [
543
                'category' => get_class($this),
544
            ]);
545
        } catch (\Exception $e) {
546
            $this->_logger->error('failed delete file node ['.$this->_id.']', [
547
                'category' => get_class($this),
548
                'exception' => $e,
549
            ]);
550
551
            throw $e;
552
        }
553
554
        return true;
555
    }
556
557
    /**
558
     * Increase version.
559
     */
560
    protected function increaseVersion(): int
561
    {
562
        $max = $this->_fs->getServer()->getMaxFileVersion();
563
        if (count($this->history) >= $max) {
564
            $del = key($this->history);
565
            $this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$this->history[$del]['version'].'] from file ['.$this->_id.']', [
566
                'category' => get_class($this),
567
            ]);
568
569
            $this->deleteVersion($this->history[$del]['version']);
570
        }
571
572
        ++$this->version;
573
574
        return $this->version;
575
    }
576
577
    /**
578
     * Pre content change checks.
579
     */
580
    protected function prePutFile(ObjectId $session): bool
581
    {
582
        if (!$this->_acl->isAllowed($this, 'w')) {
583
            throw new AclException\Forbidden('not allowed to modify node', AclException\Forbidden::NOT_ALLOWED_TO_MODIFY);
584
        }
585
586
        $this->_hook->run('prePutFile', [$this, &$session]);
587
588
        if ($this->readonly) {
589
            throw new Exception\Conflict('node is marked as readonly, it is not possible to change any content', Exception\Conflict::READONLY);
590
        }
591
592
        return true;
593
    }
594
595
    /**
596
     * Add new version.
597
     */
598
    protected function addVersion(array $attributes = []): self
599
    {
600
        if (1 !== $this->version) {
601
            $this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [
602
                'category' => get_class($this),
603
            ]);
604
605
            $this->history[] = [
606
                'version' => $this->version,
607
                'changed' => $this->changed,
608
                'user' => $this->_user->getId(),
609
                'type' => self::HISTORY_EDIT,
610
                'storage' => $this->storage,
611
                'size' => $this->size,
612
                'mime' => $this->mime,
613
                'hash' => $this->hash,
614
            ];
615
616
            return $this;
617
        }
618
619
        $this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [
620
            'category' => get_class($this),
621
        ]);
622
623
        $this->history[0] = [
624
            'version' => 1,
625
            'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(),
626
            'user' => $this->owner,
627
            'type' => self::HISTORY_CREATE,
628
            'storage' => $this->storage,
629
            'size' => $this->size,
630
            'mime' => $this->mime,
631
            'hash' => $this->hash,
632
        ];
633
634
        return $this;
635
    }
636
637
    /**
638
     * Finalize put request.
639
     */
640
    protected function postPutFile(SessionInterface $session): self
641
    {
642
        try {
643
            $this->save([
644
                'size',
645
                'changed',
646
                'deleted',
647
                'mime',
648
                'hash',
649
                'version',
650
                'history',
651
                'storage',
652
            ]);
653
654
            $this->_session_factory->deleteOne($session->getId());
655
656
            $this->_logger->debug('modifed file metadata ['.$this->_id.']', [
657
                'category' => get_class($this),
658
            ]);
659
660
            $this->_hook->run('postPutFile', [$this]);
661
662
            return $this;
663
        } catch (\Exception $e) {
664
            $this->_logger->error('failed modify file metadata ['.$this->_id.']', [
665
                'category' => get_class($this),
666
                'exception' => $e,
667
            ]);
668
669
            throw $e;
670
        }
671
    }
672
}
673