Completed
Branch dev (d5d70c)
by Raffael
11:00
created

File::validatePutRequest()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 27
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 27
rs 8.439
c 0
b 0
f 0
cc 6
eloc 15
nc 4
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * balloon
7
 *
8
 * @copyright   Copryright (c) 2012-2018 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\Exception;
15
use Balloon\Filesystem;
16
use Balloon\Filesystem\Acl;
17
use Balloon\Filesystem\Acl\Exception\Forbidden as ForbiddenException;
18
use Balloon\Filesystem\Storage;
19
use Balloon\Hook;
20
use Balloon\Mime;
21
use Balloon\Server\User;
22
use MongoDB\BSON\UTCDateTime;
23
use Psr\Log\LoggerInterface;
24
25
class File extends AbstractNode
26
{
27
    /**
28
     * History types.
29
     */
30
    const HISTORY_CREATE = 0;
31
    const HISTORY_EDIT = 1;
32
    const HISTORY_RESTORE = 2;
33
    const HISTORY_DELETE = 3;
34
    const HISTORY_UNDELETE = 4;
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
    ];
57
58
    /**
59
     * MD5 Hash of the content.
60
     *
61
     * @var string
62
     */
63
    protected $hash;
64
65
    /**
66
     * File version.
67
     *
68
     * @var int
69
     */
70
    protected $version = 0;
71
72
    /**
73
     * File size.
74
     *
75
     * @var int
76
     */
77
    protected $size = 0;
78
79
    /**
80
     * History.
81
     *
82
     * @var array
83
     */
84
    protected $history = [];
85
86
    /**
87
     * Storage.
88
     *
89
     * @var Storage
90
     */
91
    protected $_storage;
92
93
    /**
94
     * Storage attributes.
95
     *
96
     * @var mixed
97
     */
98
    protected $storage;
99
100
    /**
101
     * Initialize file node.
102
     *
103
     * @param array      $attributes
104
     * @param Filesystem $fs
105
     */
106
    public function __construct(array $attributes, Filesystem $fs, LoggerInterface $logger, Hook $hook, Acl $acl, Storage $storage)
107
    {
108
        $this->_fs = $fs;
109
        $this->_server = $fs->getServer();
110
        $this->_db = $fs->getDatabase();
111
        $this->_user = $fs->getUser();
112
        $this->_logger = $logger;
113
        $this->_hook = $hook;
114
        $this->_storage = $storage;
115
        $this->_acl = $acl;
116
117
        foreach ($attributes as $attr => $value) {
118
            $this->{$attr} = $value;
119
        }
120
121
        $this->raw_attributes = $attributes;
122
    }
123
124
    /**
125
     * Read content and return ressource.
126
     *
127
     * @return resource
128
     */
129
    public function get()
130
    {
131
        try {
132
            if (null === $this->storage) {
133
                return null;
134
            }
135
136
            return $this->_storage->getFile($this);
137
        } catch (\Exception $e) {
138
            throw new Exception\NotFound(
139
                'content not found',
140
                Exception\NotFound::CONTENTS_NOT_FOUND
141
            );
142
        }
143
    }
144
145
    /**
146
     * Copy node.
147
     *
148
     * @param Collection $parent
149
     * @param int        $conflict
150
     * @param string     $recursion
151
     * @param bool       $recursion_first
152
     *
153
     * @return NodeInterface
154
     */
155
    public function copyTo(Collection $parent, int $conflict = NodeInterface::CONFLICT_NOACTION, ?string $recursion = null, bool $recursion_first = true): NodeInterface
156
    {
157
        $this->_hook->run(
158
            'preCopyFile',
159
            [$this, $parent, &$conflict, &$recursion, &$recursion_first]
160
        );
161
162
        if (NodeInterface::CONFLICT_RENAME === $conflict && $parent->childExists($this->name)) {
163
            $name = $this->getDuplicateName();
164
        } else {
165
            $name = $this->name;
166
        }
167
168
        if (NodeInterface::CONFLICT_MERGE === $conflict && $parent->childExists($this->name)) {
169
            $result = $parent->getChild($this->name);
170
            $result->put($this->get());
171
        } else {
172
            $result = $parent->addFile($name, $this->get(), [
173
                'created' => $this->created,
174
                'changed' => $this->changed,
175
                'deleted' => $this->deleted,
176
                'meta' => $this->meta,
177
            ], NodeInterface::CONFLICT_NOACTION, true);
178
        }
179
180
        $this->_hook->run(
181
            'postCopyFile',
182
            [$this, $parent, $result, $conflict, $recursion, $recursion_first]
183
        );
184
185
        return $result;
186
    }
187
188
    /**
189
     * Get history.
190
     *
191
     * @return array
192
     */
193
    public function getHistory(): array
194
    {
195
        return $this->history;
196
    }
197
198
    /**
199
     * Restore content to some older version.
200
     *
201
     * @param int $version
202
     *
203
     * @return bool
204
     */
205
    public function restore(int $version): bool
206
    {
207
        if (!$this->_acl->isAllowed($this, 'w')) {
208
            throw new ForbiddenException(
209
                'not allowed to restore node '.$this->name,
210
                ForbiddenException::NOT_ALLOWED_TO_RESTORE
211
            );
212
        }
213
214
        $this->_hook->run('preRestoreFile', [$this, &$version]);
215
216
        if ($this->readonly) {
217
            throw new Exception\Conflict(
218
                'node is marked as readonly, it is not possible to change any content',
219
                Exception\Conflict::READONLY
220
            );
221
        }
222
223
        if ($this->version === $version) {
224
            throw new Exception('file is already version '.$version);
225
        }
226
227
        $v = array_search($version, array_column($this->history, 'version'), true);
228
        if (null === $v) {
229
            throw new Exception('failed restore file to version '.$version.', version was not found');
230
        }
231
232
        $file = $this->history[$v]['storage'];
233
        $exists = [];
234
235
        if (null !== $file) {
236
            try {
237
                $exists = $this->_storage->getFileMeta($this, $this->history[$v]['storage']);
238
            } catch (\Exception $e) {
239
                throw new Exception('could not restore to version '.$version.', version content does not exists');
240
            }
241
        }
242
243
        $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...
244
        $new = $this->increaseVersion();
245
246
        $this->history[] = [
247
            'version' => $new,
248
            'changed' => $this->changed,
249
            'user' => $this->owner,
250
            'type' => self::HISTORY_RESTORE,
251
            'origin' => $this->history[$v]['version'],
252
            'storage' => $this->history[$v]['storage'],
253
            'storage_adapter' => $this->history[$v]['storage_adapter'],
254
            'size' => $this->history[$v]['size'],
255
            'mime' => isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null,
256
        ];
257
258
        try {
259
            $this->deleted = false;
260
            $this->version = $new;
261
            $this->storage = $this->history[$v]['storage'];
262
            $this->storage_adapter = $this->history[$v]['storage_adapter'];
263
264
            $this->hash = null === $file ? self::EMPTY_CONTENT : $exists['md5'];
265
            $this->mime = isset($this->history[$v]['mime']) ? $this->history[$v]['mime'] : null;
266
            $this->size = $this->history[$v]['size'];
267
            $this->changed = $this->history[$v]['changed'];
268
269
            $this->save([
270
                'deleted',
271
                'version',
272
                'storage',
273
                'storage_adapter',
274
                'hash',
275
                'mime',
276
                'size',
277
                'history',
278
                'changed',
279
            ]);
280
281
            $this->_hook->run('postRestoreFile', [$this, &$version]);
282
283
            $this->_logger->info('restored file ['.$this->_id.'] to version ['.$version.']', [
284
                'category' => get_class($this),
285
            ]);
286
        } catch (\Exception $e) {
287
            $this->_logger->error('failed restore file ['.$this->_id.'] to version ['.$version.']', [
288
                'category' => get_class($this),
289
                'exception' => $e,
290
            ]);
291
292
            throw $e;
293
        }
294
295
        return true;
296
    }
297
298
    /**
299
     * Delete node.
300
     *
301
     * Actually the node will not be deleted (Just set a delete flag), set $force=true to
302
     * delete finally
303
     *
304
     * @param bool   $force
305
     * @param string $recursion
306
     * @param bool   $recursion_first
307
     *
308
     * @return bool
309
     */
310
    public function delete(bool $force = false, ?string $recursion = null, bool $recursion_first = true): bool
311
    {
312
        if (!$this->_acl->isAllowed($this, 'w')) {
313
            throw new ForbiddenException(
314
                'not allowed to delete node '.$this->name,
315
                ForbiddenException::NOT_ALLOWED_TO_DELETE
316
            );
317
        }
318
319
        $this->_hook->run('preDeleteFile', [$this, &$force, &$recursion, &$recursion_first]);
320
321
        if ($this->readonly && null !== $this->_user) {
322
            throw new Exception\Conflict(
323
                'node is marked as readonly, it is not possible to delete it',
324
                Exception\Conflict::READONLY
325
            );
326
        }
327
328
        if (true === $force || $this->isTemporaryFile()) {
329
            $result = $this->_forceDelete();
330
            $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
331
332
            return $result;
333
        }
334
335
        $ts = new UTCDateTime();
336
        $this->deleted = $ts;
337
        $this->increaseVersion();
338
339
        $this->history[] = [
340
            'version' => $this->version,
341
            'changed' => $ts,
342
            'user' => ($this->_user === null) ? null : $this->_user->getId(),
343
            'type' => self::HISTORY_DELETE,
344
            'storage' => $this->storage,
345
            'storage_adapter' => $this->storage_adapter,
346
            'size' => $this->size,
347
        ];
348
349
        $result = $this->save([
350
            'version',
351
            'deleted',
352
            'history',
353
        ], [], $recursion, $recursion_first);
354
355
        $this->_hook->run('postDeleteFile', [$this, $force, $recursion, $recursion_first]);
356
357
        return $result;
358
    }
359
360
    /**
361
     * Check if file is temporary.
362
     *
363
     * @return bool
364
     **/
365
    public function isTemporaryFile(): bool
366
    {
367
        foreach ($this->temp_files as $pattern) {
368
            if (preg_match($pattern, $this->name)) {
369
                return true;
370
            }
371
        }
372
373
        return false;
374
    }
375
376
    /**
377
     * Delete version.
378
     *
379
     * @param int $version
380
     *
381
     * @return bool
382
     */
383
    public function deleteVersion(int $version): bool
384
    {
385
        $key = array_search($version, array_column($this->history, 'version'), true);
386
387
        if (false === $key) {
388
            throw new Exception('version '.$version.' does not exists');
389
        }
390
391
        try {
392
            if ($this->history[$key]['storage'] !== null) {
393
                $this->_storage->deleteFile($this, $this->history[$key]['storage']);
394
            }
395
396
            array_splice($this->history, $key, 1);
397
398
            $this->_logger->debug('removed version ['.$version.'] from file ['.$this->_id.']', [
399
                'category' => get_class($this),
400
            ]);
401
402
            return $this->save('history');
403
        } catch (\Exception $e) {
404
            $this->_logger->error('failed remove version ['.$version.'] from file ['.$this->_id.']', [
405
                'category' => get_class($this),
406
                'exception' => $e,
407
            ]);
408
409
            throw $e;
410
        }
411
    }
412
413
    /**
414
     * Cleanup history.
415
     *
416
     * @return bool
417
     */
418
    public function cleanHistory(): bool
419
    {
420
        foreach ($this->history as $node) {
421
            $this->deleteVersion($node['version']);
422
        }
423
424
        return true;
425
    }
426
427
    /**
428
     * Get Attributes.
429
     *
430
     * @return array
431
     */
432
    public function getAttributes(): array
433
    {
434
        return [
435
            '_id' => $this->_id,
436
            'name' => $this->name,
437
            'hash' => $this->hash,
438
            'directory' => false,
439
            'size' => $this->size,
440
            'version' => $this->version,
441
            'parent' => $this->parent,
442
            'acl' => $this->acl,
443
            'app' => $this->app,
444
            'meta' => $this->meta,
445
            'mime' => $this->mime,
446
            'owner' => $this->owner,
447
            'history' => $this->history,
448
            'shared' => $this->shared,
449
            'deleted' => $this->deleted,
450
            'changed' => $this->changed,
451
            'created' => $this->created,
452
            'destroy' => $this->destroy,
453
            'readonly' => $this->readonly,
454
            'storage_adapter' => $this->storage_adapter,
455
            'storage' => $this->storage,
456
        ];
457
    }
458
459
    /**
460
     * Get filename extension.
461
     *
462
     * @return string
463
     */
464
    public function getExtension(): string
465
    {
466
        $ext = strrchr($this->name, '.');
467
        if (false === $ext) {
468
            throw new Exception('file does not have an extension');
469
        }
470
471
        return substr($ext, 1);
472
    }
473
474
    /**
475
     * Get file size.
476
     *
477
     * @return int
478
     */
479
    public function getSize(): int
480
    {
481
        return $this->size;
482
    }
483
484
    /**
485
     * Get md5 sum of the file content,
486
     * actually the hash value comes from the database.
487
     *
488
     * @return string
489
     */
490
    public function getETag(): string
491
    {
492
        return "'".$this->hash."'";
493
    }
494
495
    /**
496
     * Get hash.
497
     *
498
     * @return string
499
     */
500
    public function getHash(): string
501
    {
502
        return $this->hash;
503
    }
504
505
    /**
506
     * Get version.
507
     *
508
     * @return int
509
     */
510
    public function getVersion(): int
511
    {
512
        return $this->version;
513
    }
514
515
    /**
516
     * Change content.
517
     *
518
     * @param resource|string $file
519
     * @param bool            $new
520
     * @param array           $attributes
521
     *
522
     * @return int
523
     */
524
    public function put($file, bool $new = false, array $attributes = []): int
525
    {
526
        $this->_logger->debug('add contents for file ['.$this->_id.']', [
527
            'category' => get_class($this),
528
        ]);
529
530
        $this->validatePutRequest($file, $new, $attributes);
531
        $file = $this->createTemporaryFile($file, $stream);
532
        $new_hash = $this->verifyFile($file, $new);
533
534
        if ($this->hash === $new_hash) {
535
            $this->_logger->info('stop PUT execution, content checksums are equal for file ['.$this->_id.']', [
536
                'category' => get_class($this),
537
            ]);
538
539
            //Remove tmp file
540
            if (null !== $file) {
541
                unlink($file);
542
                fclose($stream);
543
            }
544
545
            return $this->version;
546
        }
547
548
        $this->hash = $new_hash;
549
        $max = (int) (string) $this->_fs->getServer()->getMaxFileVersion();
550
        if (count($this->history) >= $max) {
551
            $del = key($this->history);
552
            $this->_logger->debug('history limit ['.$max.'] reached, remove oldest version ['.$del.'] from file ['.$this->_id.']', [
553
                'category' => get_class($this),
554
            ]);
555
556
            $this->deleteVersion($this->history[$del]['version']);
557
        }
558
559
        //Write new content
560
        if ($this->size > 0) {
561
            $this->storage = $this->_storage->storeFile($this, $stream, $this->storage_adapter);
562
        } else {
563
            $this->storage = null;
564
        }
565
566
        //Update current version
567
        $this->increaseVersion();
568
569
        //Get meta attributes
570
        if (isset($attributes['mime'])) {
571
            $this->mime = $attributes['mime'];
572
        } elseif (null !== $file) {
573
            $this->mime = (new Mime())->getMime($file, $this->name);
574
        }
575
576
        //Remove tmp file
577
        if (null !== $file) {
578
            unlink($file);
579
            fclose($stream);
580
        }
581
582
        $this->_logger->debug('set mime ['.$this->mime.'] for content, file=['.$this->_id.']', [
583
            'category' => get_class($this),
584
        ]);
585
586
        $this->addVersion($attributes)
587
             ->postPutFile($file, $new, $attributes);
588
589
        return $this->version;
590
    }
591
592
    /**
593
     * Completly remove file.
594
     *
595
     * @return bool
596
     */
597
    protected function _forceDelete(): bool
598
    {
599
        try {
600
            $this->cleanHistory();
601
            $this->_db->storage->deleteOne([
602
                '_id' => $this->_id,
603
            ]);
604
605
            $this->_logger->info('removed file node ['.$this->_id.']', [
606
                'category' => get_class($this),
607
            ]);
608
        } catch (\Exception $e) {
609
            $this->_logger->error('failed delete file node ['.$this->_id.']', [
610
                'category' => get_class($this),
611
                'exception' => $e,
612
            ]);
613
614
            throw $e;
615
        }
616
617
        return true;
618
    }
619
620
    /**
621
     * Increase version.
622
     *
623
     * @return int
624
     */
625
    protected function increaseVersion(): int
626
    {
627
        ++$this->version;
628
629
        return $this->version;
630
    }
631
632
    /**
633
     * Create uuidv4.
634
     *
635
     * @param string $data
636
     *
637
     * @return string
638
     */
639
    protected function guidv4(string $data): string
640
    {
641
        assert(16 === strlen($data));
642
643
        $data[6] = chr(ord($data[6]) & 0x0f | 0x40); // set version to 0100
644
        $data[8] = chr(ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
645
646
        return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
647
    }
648
649
    /**
650
     * Change content.
651
     *
652
     * @param resource|string $file
653
     * @param bool            $new
654
     * @param array           $attributes
655
     *
656
     * @return bool
657
     */
658
    protected function validatePutRequest($file, bool $new = false, array $attributes = []): bool
659
    {
660
        if (!$this->_acl->isAllowed($this, 'w')) {
661
            throw new ForbiddenException(
662
                'not allowed to modify node',
663
                ForbiddenException::NOT_ALLOWED_TO_MODIFY
664
            );
665
        }
666
667
        $this->_hook->run('prePutFile', [$this, &$file, &$new, &$attributes]);
668
669
        if ($this->readonly) {
670
            throw new Exception\Conflict(
671
                'node is marked as readonly, it is not possible to change any content',
672
                Exception\Conflict::READONLY
673
            );
674
        }
675
676
        if ($this->isShareMember() && false === $new && 'w' === $this->_acl->getAclPrivilege($this->getShareNode())) {
0 ignored issues
show
Bug introduced by
It seems like $this->getShareNode() can be null; however, getAclPrivilege() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
677
            throw new ForbiddenException(
678
                'not allowed to overwrite node',
679
                ForbiddenException::NOT_ALLOWED_TO_OVERWRITE
680
            );
681
        }
682
683
        return true;
684
    }
685
686
    /**
687
     * Verify content to be added.
688
     *
689
     * @param string $path
690
     * @param bool   $new
691
     *
692
     * @return bool
693
     */
694
    protected function verifyFile(?string $path, bool $new = false): string
695
    {
696
        if (null === $path) {
697
            $this->size = 0;
698
            $new_hash = self::EMPTY_CONTENT;
699
        } else {
700
            $size = filesize($path);
701
            $this->size = $size;
702
            $new_hash = md5_file($path);
703
704
            if (!$this->_user->checkQuota($size)) {
705
                $this->_logger->warning('could not execute PUT, user quota is full', [
706
                    'category' => get_class($this),
707
                ]);
708
709
                if (true === $new) {
710
                    $this->_forceDelete();
711
                }
712
713
                throw new Exception\InsufficientStorage(
714
                    'user quota is full',
715
                    Exception\InsufficientStorage::USER_QUOTA_FULL
716
                );
717
            }
718
        }
719
720
        return $new_hash;
721
    }
722
723
    /**
724
     * Create temporary file.
725
     *
726
     * @param resource|string $file
727
     * @param resource        $stream
728
     *
729
     * @return string
730
     */
731
    protected function createTemporaryFile($file, &$stream): ?string
732
    {
733
        if (is_string($file)) {
734
            if (!is_readable($file)) {
735
                throw new Exception('file does not exists or is not readable');
736
            }
737
738
            $stream = fopen($file, 'r');
739
        } elseif (is_resource($file)) {
740
            $tmp = $this->_fs->getServer()->getTempDir().DIRECTORY_SEPARATOR.'upload'.DIRECTORY_SEPARATOR.$this->_user->getId();
741
            if (!file_exists($tmp)) {
742
                mkdir($tmp, 0700, true);
743
            }
744
745
            $tmp_file = $tmp.DIRECTORY_SEPARATOR.$this->guidv4(openssl_random_pseudo_bytes(16));
746
            $stream = fopen($tmp_file, 'w+');
747
            $size = stream_copy_to_stream($file, $stream, ((int) $this->_fs->getServer()->getMaxFileSize() + 1));
748
            rewind($stream);
749
            fclose($file);
750
751
            if ($size > (int) $this->_fs->getServer()->getMaxFileSize()) {
752
                unlink($tmp_file);
753
754
                throw new Exception\InsufficientStorage(
755
                    'file size exceeded limit',
756
                    Exception\InsufficientStorage::FILE_SIZE_LIMIT
757
                );
758
            }
759
760
            $file = $tmp_file;
761
        } else {
762
            $file = null;
763
        }
764
765
        return $file;
766
    }
767
768
    /**
769
     * Add new version.
770
     *
771
     * @param array $attributes
772
     *
773
     * @return File
774
     */
775
    protected function addVersion(array $attributes = []): self
776
    {
777
        if (1 !== $this->version) {
778
            if (isset($attributes['changed'])) {
779
                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...
780
                    throw new Exception\InvalidArgument('attribute changed must be an instance of UTCDateTime');
781
                }
782
783
                $this->changed = $attributes['changed'];
784
            } else {
785
                $this->changed = new UTCDateTime();
786
            }
787
788
            $this->_logger->debug('added new history version ['.$this->version.'] for file ['.$this->_id.']', [
789
                'category' => get_class($this),
790
            ]);
791
792
            $this->history[] = [
793
                'version' => $this->version,
794
                'changed' => $this->changed,
795
                'user' => $this->_user->getId(),
796
                'type' => self::HISTORY_EDIT,
797
                'storage' => $this->storage,
798
                'storage_adapter' => $this->storage_adapter,
799
                'size' => $this->size,
800
                'mime' => $this->mime,
801
            ];
802
        } else {
803
            $this->_logger->debug('added first file version [1] for file ['.$this->_id.']', [
804
                'category' => get_class($this),
805
            ]);
806
807
            $this->history[0] = [
808
                'version' => 1,
809
                'changed' => isset($attributes['changed']) ? $attributes['changed'] : new UTCDateTime(),
810
                'user' => $this->owner,
811
                'type' => self::HISTORY_CREATE,
812
                'storage' => $this->storage,
813
                'storage_adapter' => $this->storage_adapter,
814
                'size' => $this->size,
815
                'mime' => $this->mime,
816
            ];
817
        }
818
819
        return $this;
820
    }
821
822
    /**
823
     * Finalize put request.
824
     *
825
     * @param resource|string $file
826
     * @param bool            $new
827
     * @param array           $attributes
828
     *
829
     * @return File
830
     */
831
    protected function postPutFile($file, bool $new, array $attributes): self
832
    {
833
        try {
834
            $this->save([
835
                'size',
836
                'changed',
837
                'mime',
838
                'hash',
839
                'version',
840
                'history',
841
                'storage',
842
                'storage_adapter',
843
            ]);
844
845
            $this->_logger->debug('modifed file metadata ['.$this->_id.']', [
846
                'category' => get_class($this),
847
            ]);
848
849
            $this->_hook->run('postPutFile', [$this, $file, $new, $attributes]);
850
851
            return $this;
852
        } catch (\Exception $e) {
853
            $this->_logger->error('failed modify file metadata ['.$this->_id.']', [
854
                'category' => get_class($this),
855
                'exception' => $e,
856
            ]);
857
858
            throw $e;
859
        }
860
    }
861
}
862