Completed
Pull Request — master (#141)
by Raffael
15:37 queued 10:36
created

Gridfs::storeStream()   B

Complexity

Conditions 6
Paths 10

Size

Total Lines 93

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 0
Metric Value
dl 0
loc 93
ccs 0
cts 74
cp 0
rs 7.5305
c 0
b 0
f 0
cc 6
nc 10
nop 4
crap 42

How to fix   Long Method   

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-2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
10
 */
11
12
namespace Balloon\Filesystem\Storage\Adapter;
13
14
use Balloon\Filesystem\Node\Collection;
15
use Balloon\Filesystem\Node\File;
16
use Balloon\Filesystem\Node\NodeInterface;
17
use Balloon\Filesystem\Storage\Exception;
18
use Balloon\Server\User;
19
use MongoDB\BSON\Binary;
20
use MongoDB\BSON\ObjectId;
21
use MongoDB\BSON\UTCDateTime;
22
use MongoDB\Database;
23
use MongoDB\GridFS\Bucket;
24
use Psr\Log\LoggerInterface;
25
26
class Gridfs implements AdapterInterface
27
{
28
    /**
29
     * Grid chunks.
30
     */
31
    public const CHUNK_SIZE = 261120;
32
33
    /**
34
     * Database.
35
     *
36
     * @var Database
37
     */
38
    protected $db;
39
40
    /**
41
     * GridFS.
42
     *
43
     * @var Bucket
44
     */
45
    protected $gridfs;
46
47
    /**
48
     * Logger.
49
     *
50
     * @var LoggerInterface
51
     */
52
    protected $logger;
53
54
    /**
55
     * GridFS storage.
56
     *
57
     * @param Database
58
     */
59
    public function __construct(Database $db, LoggerInterface $logger)
60
    {
61
        $this->db = $db;
62
        $this->gridfs = $db->selectGridFSBucket();
63
        $this->logger = $logger;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function hasNode(NodeInterface $node, array $attributes): bool
70
    {
71
        return null !== $this->getFileById($attributes);
0 ignored issues
show
Documentation introduced by
$attributes is of type array, but the function expects a object<MongoDB\BSON\ObjectId>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
72
    }
73
74
    /**
75
     * {@inheritdoc}
76
     */
77
    public function deleteFile(File $file, array $attributes): bool
78
    {
79
        if (!isset($attributes['_id'])) {
80
            throw new Exception\BlobNotFound('attributes do not contain a gridfs id');
81
        }
82
83
        $exists = $this->getFileById($attributes['_id']);
84
85
        if (null === $exists) {
86
            $this->logger->debug('gridfs content node ['.$exists['_id'].'] was not found, file reference=['.$file->getId().']', [
87
                'category' => get_class($this),
88
            ]);
89
90
            return false;
91
        }
92
93
        if (!isset($exists['metadata']['references'])) {
94
            $this->gridfs->delete($exists['_id']);
95
96
            return true;
97
        }
98
99
        $refs = $exists['metadata']['references'];
100
        if (($key = array_search($file->getId(), $refs)) !== false) {
101
            unset($refs[$key]);
102
            $refs = array_values($refs);
103
        }
104
105
        if (count($refs) >= 1) {
106
            $this->logger->debug('gridfs content node ['.$exists['_id'].'] still has references left, just remove the reference ['.$file->getId().']', [
107
                'category' => get_class($this),
108
            ]);
109
110
            $this->db->{'fs.files'}->updateOne(['_id' => $exists['_id']], [
111
                '$set' => ['metadata.references' => $refs],
112
            ]);
113
        } else {
114
            $this->logger->debug('gridfs content node ['.$exists['_id'].'] has no references left, delete node completely', [
115
                'category' => get_class($this),
116
            ]);
117
118
            $this->gridfs->delete($exists['_id']);
119
        }
120
121
        return true;
122
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127
    public function getFile(File $file, array $attributes)
128
    {
129
        if (!isset($attributes['_id'])) {
130
            throw new Exception\BlobNotFound('attributes do not contain a gridfs id');
131
        }
132
133
        return $this->gridfs->openDownloadStream($attributes['_id']);
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139
    public function storeFile(File $file, ObjectId $session): array
140
    {
141
        $this->logger->debug('finalize temporary file ['.$session.'] and add file ['.$file->getId().'] as reference', [
142
            'category' => get_class($this),
143
        ]);
144
145
        $md5 = $this->db->command([
146
            'filemd5' => $session,
147
            'root' => 'fs',
148
        ])->toArray()[0]['md5'];
149
150
        $blob = $this->getFileByHash($md5);
151
152
        if ($blob !== null) {
153
            $this->logger->debug('found existing file with hash ['.$md5.'], add file ['.$file->getId().'] as reference to ['.$blob['_id'].']', [
154
                'category' => get_class($this),
155
            ]);
156
157
            $this->db->selectCollection('fs.files')->updateOne([
158
                'md5' => $blob['md5'],
159
            ], [
160
                '$addToSet' => [
161
                    'metadata.references' => $file->getId(),
162
                ],
163
            ]);
164
165
            $this->gridfs->delete($session);
166
167
            return [
168
                'reference' => ['_id' => $blob['_id']],
169
                'size' => $blob['length'],
170
                'hash' => $md5,
171
            ];
172
        }
173
174
        $this->logger->debug('calculated hash ['.$md5.'] for temporary file ['.$session.'], remove temporary flag', [
175
            'category' => get_class($this),
176
        ]);
177
178
        $this->db->selectCollection('fs.files')->updateOne([
179
            '_id' => $session,
180
        ], [
181
            '$set' => [
182
                'md5' => $md5,
183
                'metadata.references' => [$file->getId()],
184
            ],
185
            '$unset' => [
186
                'metadata.temporary' => true,
187
            ],
188
        ]);
189
190
        $blob = $this->getFileById($session);
191
192
        return [
193
            'reference' => ['_id' => $session],
194
            'size' => $blob['length'],
195
            'hash' => $md5,
196
        ];
197
    }
198
199
    /**
200
     * Create collection.
201
     */
202
    public function createCollection(Collection $collection): array
203
    {
204
        return [];
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function storeTemporaryFile($stream, User $user, ?ObjectId $session = null): ObjectId
211
    {
212
        $exists = $session;
213
214
        if ($session === null) {
215
            $session = new ObjectId();
216
217
            $this->logger->info('create new tempory storage file ['.$session.']', [
218
                'category' => get_class($this),
219
            ]);
220
221
            $this->db->selectCollection('fs.files')->insertOne([
222
                '_id' => $session,
223
                'chunkSize' => self::CHUNK_SIZE,
224
                'length' => 0,
225
                'uploadDate' => new UTCDateTime(),
226
                'metadata' => ['temporary' => true],
227
            ]);
228
        }
229
230
        $temp = $this->db->selectCollection('fs.files')->findOne([
231
            '_id' => $session,
232
        ]);
233
234
        if ($temp === null) {
235
            throw new Exception\SessionNotFound('Temporary storage for this file is gone');
236
        }
237
238
        $this->storeStream($stream, $user, $exists, $temp);
239
240
        return $session;
241
    }
242
243
    /**
244
     * Get stored file.
245
     */
246
    protected function getFileById(ObjectId $id): ?array
247
    {
248
        return $this->gridfs->findOne(['_id' => $id]);
249
    }
250
251
    /**
252
     * Get stored file.
253
     */
254
    protected function getFileByHash(string $hash): ?array
255
    {
256
        return $this->gridfs->findOne(['md5' => $hash]);
257
    }
258
259
    /**
260
     * Store stream content.
261
     */
262
    protected function storeStream($stream, User $user, ?ObjectId $exists = null, array $temp): int
263
    {
264
        $data = null;
265
        $length = $temp['length'];
266
        $chunks = 0;
267
        $left = 0;
268
269
        if ($exists !== null) {
270
            $chunks = (int) ceil($temp['length'] / $temp['chunkSize']);
271
            $left = (int) ($chunks * $temp['chunkSize'] - $temp['length']);
272
273
            if ($left < 0) {
274
                $left = 0;
275
            } else {
276
                $this->logger->debug('found existing chunks ['.$chunks.'] for temporary file ['.$temp['_id'].'] while the last chunk has ['.$left.'] bytes free', [
277
                    'category' => get_class($this),
278
                ]);
279
280
                --$chunks;
281
282
                $last = $this->db->selectCollection('fs.chunks')->findOne([
283
                    'files_id' => $temp['_id'],
284
                    'n' => $chunks,
285
                ]);
286
287
                if ($last === null) {
288
                    throw new Exception\ChunkNotFound('Chunk not found, file is corrupt');
289
                }
290
291
                $data = $last['data']->getData();
292
            }
293
        }
294
295
        while (!feof($stream)) {
296
            if ($data !== null) {
297
                $append = $this->readStream($stream, $left);
298
                $data .= $append;
299
                $chunk = new Binary($data, Binary::TYPE_GENERIC);
300
                $size = mb_strlen($append, '8bit');
301
                $length += $size;
302
303
                $this->logger->debug('append data ['.$size.'] to last chunk ['.$chunks.'] in temporary file ['.$temp['_id'].']', [
304
                    'category' => get_class($this),
305
                ]);
306
307
                $last = $this->db->selectCollection('fs.chunks')->updateOne([
0 ignored issues
show
Unused Code introduced by
$last 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...
308
                    'files_id' => $temp['_id'],
309
                    'n' => $chunks,
310
                ], [
311
                    '$set' => [
312
                        'data' => $chunk,
313
                    ],
314
                ]);
315
316
                ++$chunks;
317
                $data = null;
318
319
                continue;
320
            }
321
322
            $content = $this->readStream($stream, $temp['chunkSize']);
323
            $size = mb_strlen($content, '8bit');
324
            $length += $size;
325
326
            $chunk = new Binary($content, Binary::TYPE_GENERIC);
327
            $last = $this->db->selectCollection('fs.chunks')->insertOne([
328
                'files_id' => $temp['_id'],
329
                'n' => $chunks,
330
                'data' => $chunk,
331
            ]);
332
333
            $this->logger->debug('inserted new chunk ['.$last->getInsertedId().'] ['.$size.'] in temporary file ['.$temp['_id'].']', [
334
                'category' => get_class($this),
335
            ]);
336
337
            ++$chunks;
338
339
            continue;
340
        }
341
342
        $this->verifyQuota($user, $length, $temp['_id']);
343
344
        $this->db->selectCollection('fs.files')->updateOne([
345
            '_id' => $temp['_id'],
346
        ], [
347
            '$set' => [
348
                'uploadDate' => new UTCDateTime(),
349
                'length' => $length,
350
            ],
351
        ]);
352
353
        return $length;
354
    }
355
356
    /**
357
     * Verify quota.
358
     */
359
    protected function verifyQuota(User $user, int $size, ObjectId $session): bool
360
    {
361
        if (!$user->checkQuota($size)) {
362
            $this->logger->warning('stop adding chunk, user ['.$user->getId().'] quota is full, remove upload session', [
363
                'category' => get_class($this),
364
            ]);
365
366
            $this->gridfs->delete($session);
367
368
            throw new Exception\InsufficientStorage(
369
                'user quota is full',
370
                 Exception\InsufficientStorage::USER_QUOTA_FULL
371
            );
372
        }
373
374
        return true;
375
    }
376
377
    /**
378
     * Read x bytes from stream.
379
     */
380
    protected function readStream($stream, int $bytes): string
381
    {
382
        $length = 0;
383
        $data = '';
384
        while (!feof($stream)) {
385
            if ($length + 8192 > $bytes) {
386
                $max = $bytes - $length;
387
388
                if ($max === 0) {
389
                    return $data;
390
                }
391
392
                $length += $max;
393
394
                return $data .= fread($stream, $max);
395
            }
396
397
            $length += 8192;
398
            $data .= fread($stream, 8192);
399
        }
400
401
        $this->logger->warning('read:'.$bytes.'&/'.$length, [
402
                'category' => get_class($this),
403
            ]);
404
405
        return $data;
406
    }
407
}
408