Test Failed
Push — master ( 4a1cb1...96b0ee )
by frey
01:55
created

Adapter   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 531
Duplicated Lines 0 %

Test Coverage

Coverage 82.47%

Importance

Changes 0
Metric Value
eloc 164
dl 0
loc 531
ccs 160
cts 194
cp 0.8247
rs 3.6
c 0
b 0
f 0
wmc 60

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A write() 0 5 1
A getMetadata() 0 6 1
A rename() 0 7 1
A getVisibility() 0 17 5
A copy() 0 5 1
A getTimestamp() 0 6 2
A updateStream() 0 3 1
A listObjects() 0 6 3
A getMimetype() 0 6 2
A getAppId() 0 3 1
A getHttpClient() 0 6 1
A delete() 0 5 1
A normalizeFileInfo() 0 13 3
A getBucket() 0 3 1
A readStream() 0 15 3
A has() 0 6 2
A getBucketWithAppId() 0 3 1
A read() 0 20 5
A deleteDir() 0 15 2
A getTemporaryUrl() 0 16 3
A listContents() 0 11 2
A update() 0 3 1
A getSourcePath() 0 4 1
A createDir() 0 6 1
A prepareUploadConfig() 0 13 3
A setVisibility() 0 6 1
A getSize() 0 6 2
A normalizeVisibility() 0 9 2
A writeStream() 0 9 1
A getRegion() 0 4 2
A getUrl() 0 17 3

How to fix   Complexity   

Complex Class

Complex classes like Adapter 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.

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 Adapter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Freyo\Flysystem\QcloudCOSv5;
4
5
use Carbon\Carbon;
6
use DateTimeInterface;
7
use League\Flysystem\Adapter\AbstractAdapter;
8
use League\Flysystem\Adapter\CanOverwriteFiles;
9
use League\Flysystem\AdapterInterface;
10
use League\Flysystem\Config;
11
use Qcloud\Cos\Client;
12
use Qcloud\Cos\Exception\NoSuchKeyException;
13
14
/**
15
 * Class Adapter.
16
 */
17
class Adapter extends AbstractAdapter implements CanOverwriteFiles
18
{
19
    /**
20
     * @var Client
21
     */
22
    protected $client;
23
24
    /**
25
     * @var array
26
     */
27
    protected $config = [];
28
29
    /**
30
     * @var array
31
     */
32
    protected $regionMap = [
33
        'cn-east'      => 'ap-shanghai',
34
        'cn-sorth'     => 'ap-guangzhou',
35
        'cn-north'     => 'ap-beijing-1',
36
        'cn-south-2'   => 'ap-guangzhou-2',
37
        'cn-southwest' => 'ap-chengdu',
38
        'sg'           => 'ap-singapore',
39
        'tj'           => 'ap-beijing-1',
40
        'bj'           => 'ap-beijing',
41
        'sh'           => 'ap-shanghai',
42
        'gz'           => 'ap-guangzhou',
43
        'cd'           => 'ap-chengdu',
44
        'sgp'          => 'ap-singapore',
45
    ];
46
47
    /**
48
     * Adapter constructor.
49
     *
50
     * @param Client $client
51
     * @param array  $config
52
     */
53
    public function __construct(Client $client, array $config)
54
    {
55
        $this->client = $client;
56
        $this->config = $config;
57
58
        $this->setPathPrefix($config['cdn']);
59
    }
60
    
61
    /**
62
     * @return string
63
     */
64 19
    public function getBucketWithAppId()
65 1
    {
66 19
        return $this->getBucket().'-'.$this->getAppId();
67
    }
68
    
69
    /**
70
     * @return string
71
     */
72 2
    public function getBucket()
73
    {
74 2
        return str_replace('-'.getAppId(), '', $this->config['bucket']);
0 ignored issues
show
Bug introduced by
The function getAppId was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

74
        return str_replace('-'./** @scrutinizer ignore-call */ getAppId(), '', $this->config['bucket']);
Loading history...
75
    }
76
77
    /**
78
     * @return string
79
     */
80 2
    public function getAppId()
81
    {
82 2
        return $this->config['credentials']['appId'];
83 2
    }
84
85
    /**
86
     * @return string
87
     */
88
    public function getRegion()
89
    {
90
        return array_key_exists($this->config['region'], $this->regionMap)
91 2
            ? $this->regionMap[$this->config['region']] : $this->config['region'];
92
    }
93 2
94 2
    /**
95 2
     * @param $path
96
     *
97
     * @return string
98
     */
99
    public function getSourcePath($path)
100
    {
101
        return sprintf('%s-%s.cos.%s.myqcloud.com/%s',
102
            $this->getBucket(), $this->getAppId(), $this->getRegion(), $path
103 4
        );
104 4
    }
105 3
106 3
    /**
107
     * @param string $path
108
     *
109
     * @return string
110 4
     */
111
    public function getUrl($path)
112
    {
113 4
        if ($this->config['cdn']) {
114
            return $this->applyPathPrefix($path);
115
        }
116
117
        $options = [
118
            'Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http'
119
        ];
120
121
        $objectUrl = $this->client->getObjectUrl(
122
            $this->getBucket(), $path, null, $options
123
        );
124
125
        $url = parse_url($objectUrl);
126
127
        return sprintf('%s://%s%s', $url['scheme'], $url['host'], urldecode($url['path']));
128
    }
129 1
130
    /**
131 1
     * @param string             $path
132 1
     * @param \DateTimeInterface $expiration
133
     * @param array              $options
134 1
     *
135 1
     * @return string
136 1
     */
137
    public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
138 1
    {
139
        $options = array_merge($options,
140 1
            ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']);
141 1
142
        $objectUrl = $this->client->getObjectUrl(
143
            $this->getBucket(), $path, $expiration->format('c'), $options
144
        );
145
146
        $url = parse_url($objectUrl);
147
148
        if ($this->config['cdn']) {
149
            return $this->config['cdn'].urldecode($url['path']).'?'.$url['query'];
150
        }
151
152
        return sprintf('%s://%s%s?%s', $url['scheme'], $url['host'], urldecode($url['path']), $url['query']);
153
    }
154 2
155
    /**
156 2
     * @param string $path
157
     * @param string $contents
158 2
     * @param Config $config
159
     *
160
     * @return array|bool
161
     */
162
    public function write($path, $contents, Config $config)
163
    {
164
        $options = $this->prepareUploadConfig($config);
165
166
        return $this->client->upload($this->getBucket(), $path, $contents, $options);
167
    }
168 2
169 2
    /**
170 2
     * @param string   $path
171
     * @param resource $resource
172 2
     * @param Config   $config
173 2
     *
174 2
     * @return array|bool
175 2
     */
176 2
    public function writeStream($path, $resource, Config $config)
177 2
    {
178
        $options = $this->prepareUploadConfig($config);
179
180
        return $this->client->upload(
181
            $this->getBucket(),
182
            $path,
183
            stream_get_contents($resource, -1, 0),
184
            $options
185
        );
186
    }
187 1
188
    /**
189 1
     * @param string $path
190
     * @param string $contents
191
     * @param Config $config
192
     *
193
     * @return array|bool
194
     */
195
    public function update($path, $contents, Config $config)
196
    {
197
        return $this->write($path, $contents, $config);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->write($path, $contents, $config) also could return the type boolean which is incompatible with the return type mandated by League\Flysystem\AdapterInterface::update() of array|false.
Loading history...
198
    }
199 1
200
    /**
201 1
     * @param string   $path
202
     * @param resource $resource
203
     * @param Config   $config
204
     *
205
     * @return array|bool
206
     */
207
    public function updateStream($path, $resource, Config $config)
208
    {
209
        return $this->writeStream($path, $resource, $config);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->writeStrea...th, $resource, $config) also could return the type boolean which is incompatible with the return type mandated by League\Flysystem\AdapterInterface::updateStream() of array|false.
Loading history...
210 1
    }
211
212 1
    /**
213
     * @param string $path
214 1
     * @param string $newpath
215
     *
216 1
     * @return bool
217
     */
218
    public function rename($path, $newpath)
219
    {
220
        $result = $this->copy($path, $newpath);
221
222
        $this->delete($path);
223
224
        return $result;
225 2
    }
226
227 2
    /**
228
     * @param string $path
229 2
     * @param string $newpath
230
     *
231
     * @return bool
232
     */
233
    public function copy($path, $newpath)
234
    {
235
        $source = $this->getSourcePath($path);
236
237 2
        return (bool) $this->client->copy($this->getBucket(), $newpath, $source);
238
    }
239 2
240 2
    /**
241 2
     * @param string $path
242 2
     *
243
     * @return bool
244
     */
245
    public function delete($path)
246
    {
247
        return (bool) $this->client->deleteObject([
248
            'Bucket' => $this->getBucket(),
249
            'Key'    => $path,
250 1
        ]);
251
    }
252 1
253
    /**
254 1
     * @param string $dirname
255
     *
256
     * @return bool
257
     */
258 1
    public function deleteDir($dirname)
259 1
    {
260 1
        $response = $this->listObjects($dirname);
261
262 1
        if (!isset($response['Contents'])) {
263 1
            return true;
264 1
        }
265 1
266
        $keys = array_map(function ($item) {
267
            return ['Key' => $item['Key']];
268
        }, (array) $response['Contents']);
269
270
        return (bool) $this->client->deleteObjects([
271
            'Bucket'  => $this->getBucket(),
272
            'Objects' => $keys,
273
        ]);
274 1
    }
275
276 1
    /**
277 1
     * @param string $dirname
278 1
     * @param Config $config
279 1
     *
280 1
     * @return array|bool
281
     */
282
    public function createDir($dirname, Config $config)
283
    {
284
        return $this->client->putObject([
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->client->pu...e . '/', 'Body' => '')) returns the type Guzzle\Http\Message\Response which is incompatible with the documented return type array|boolean.
Loading history...
285
            'Bucket' => $this->getBucket(),
286
            'Key'    => $dirname.'/',
287
            'Body'   => '',
288
        ]);
289 1
    }
290
291 1
    /**
292 1
     * @param string $path
293 1
     * @param string $visibility
294 1
     *
295 1
     * @return bool
296
     */
297
    public function setVisibility($path, $visibility)
298
    {
299
        return (bool) $this->client->PutObjectAcl([
0 ignored issues
show
Bug Best Practice introduced by
The expression return (bool)$this->clie...sibility($visibility))) returns the type boolean which is incompatible with the return type mandated by League\Flysystem\AdapterInterface::setVisibility() of array|false.

In the issue above, the returned value is violating the contract defined by the mentioned interface.

Let's take a look at an example:

interface HasName {
    /** @return string */
    public function getName();
}

class Name {
    public $name;
}

class User implements HasName {
    /** @return string|Name */
    public function getName() {
        return new Name('foo'); // This is a violation of the ``HasName`` interface
                                // which only allows a string value to be returned.
    }
}
Loading history...
300
            'Bucket' => $this->getBucket(),
301
            'Key'    => $path,
302
            'ACL'    => $this->normalizeVisibility($visibility),
303 1
        ]);
304
    }
305
306 1
    /**
307
     * @param string $path
308
     *
309
     * @return bool
310
     */
311
    public function has($path)
312
    {
313
        try {
314
            return (bool) $this->getMetadata($path);
315
        } catch (NoSuchKeyException $e) {
316
            return false;
317 1
        }
318
    }
319
320 1
    /**
321
     * @param string $path
322
     *
323
     * @return array|bool
324
     */
325
    public function read($path)
326 1
    {
327 1
        try {
328 1
            if (isset($this->config['read_from_cdn']) && $this->config['read_from_cdn']) {
329 1
                $response = $this->getHttpClient()
330
                                 ->get($this->getTemporaryUrl($path, Carbon::now()->addMinutes(5)))
331
                                 ->getBody()
332 1
                                 ->getContents();
333
            } else {
334
                $response = $this->client->getObject([
335
                    'Bucket' => $this->getBucket(),
336
                    'Key'    => $path,
337
                ])->get('Body');
0 ignored issues
show
Bug introduced by
The method get() does not exist on Guzzle\Http\Message\Response. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

337
                ])->/** @scrutinizer ignore-call */ get('Body');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
338
            }
339
340
            return ['contents' => (string)$response];
341
        } catch (NoSuchKeyException $e) {
342
            return false;
343 1
        } catch (\GuzzleHttp\Exception\ClientException $e) {
344
            return false;
345 1
        }
346 1
    }
347 1
348 1
    /**
349 1
     * @return \GuzzleHttp\Client
350
     */
351
    public function getHttpClient()
352
    {
353
        return $client = (new \GuzzleHttp\Client([
0 ignored issues
show
Unused Code introduced by
The assignment to $client is dead and can be removed.
Loading history...
354
            'verify'          => false,
355
            'timeout'         => $this->config['timeout'],
356
            'connect_timeout' => $this->config['connect_timeout'],
357 1
        ]));
358
    }
359
360 1
    /**
361
     * @param string $path
362 1
     *
363 1
     * @return array|bool
364 1
     */
365 1
    public function readStream($path)
366
    {
367 1
        try {
368
            $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
369
370
            $stream = $this->getHttpClient()
371
                           ->get($temporaryUrl, ['stream' => true])
372
                           ->getBody()
373
                           ->detach();
374
375
            return ['stream' => $stream];
376
        } catch (NoSuchKeyException $e) {
377
            return false;
378
        } catch (\GuzzleHttp\Exception\ClientException $e) {
379
            return false;
380
        }
381 1
    }
382
383 1
    /**
384
     * @param string $directory
385 1
     * @param bool   $recursive
386
     *
387 1
     * @return array|bool
388 1
     */
389 1
    public function listContents($directory = '', $recursive = false)
390
    {
391 1
        $list = [];
392
393
        $response = $this->listObjects($directory, $recursive);
394
395
        foreach ((array) $response->get('Contents') as $content) {
396
            $list[] = $this->normalizeFileInfo($content);
397
        }
398
399 5
        return $list;
400
    }
401 5
402 5
    /**
403 5
     * @param string $path
404 5
     *
405
     * @return array|bool
406
     */
407
    public function getMetadata($path)
408
    {
409
        return $this->client->headObject([
410
            'Bucket' => $this->getBucket(),
411
            'Key'    => $path,
412 1
        ])->toArray();
0 ignored issues
show
Bug introduced by
The method toArray() does not exist on Guzzle\Http\Message\Response. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

412
        ])->/** @scrutinizer ignore-call */ toArray();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
413
    }
414 1
415
    /**
416 1
     * @param string $path
417 1
     *
418
     * @return array|bool
419
     */
420
    public function getSize($path)
421
    {
422
        $meta = $this->getMetadata($path);
423
424
        return isset($meta['ContentLength'])
425 1
            ? ['size' => $meta['ContentLength']] : false;
426
    }
427 1
428
    /**
429 1
     * @param string $path
430 1
     *
431
     * @return array|bool
432
     */
433
    public function getMimetype($path)
434
    {
435
        $meta = $this->getMetadata($path);
436
437
        return isset($meta['ContentType'])
438 1
            ? ['mimetype' => $meta['ContentType']] : false;
439
    }
440 1
441
    /**
442 1
     * @param string $path
443 1
     *
444
     * @return array|bool
445
     */
446
    public function getTimestamp($path)
447
    {
448
        $meta = $this->getMetadata($path);
449
450
        return isset($meta['LastModified'])
451 1
            ? ['timestamp' => strtotime($meta['LastModified'])] : false;
452
    }
453 1
454 1
    /**
455 1
     * @param string $path
456 1
     *
457
     * @return array|bool
458 1
     */
459 1
    public function getVisibility($path)
460 1
    {
461 1
        $meta = $this->client->getObjectAcl([
462 1
            'Bucket' => $this->getBucket(),
463
            'Key'    => $path,
464
        ]);
465 1
466
        foreach ($meta->get('Grants') as $grant) {
467 1
            if (isset($grant['Grantee']['URI'])
468
                && $grant['Permission'] === 'READ'
469
                && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
470
            ) {
471
                return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
472
            }
473
        }
474
475 1
        return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
476
    }
477 1
478
    /**
479
     * @param array $content
480 1
     *
481 1
     * @return array
482 1
     */
483 1
    private function normalizeFileInfo(array $content)
484 1
    {
485 1
        $path = pathinfo($content['Key']);
486 1
487 1
        return [
488 1
            'type'      => substr($content['Key'], -1) === '/' ? 'dir' : 'file',
489
            'path'      => $content['Key'],
490
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
491
            'size'      => (int) $content['Size'],
492
            'dirname'   => (string) $path['dirname'],
493
            'basename'  => (string) $path['basename'],
494
            'extension' => isset($path['extension']) ? $path['extension'] : '',
495
            'filename'  => (string) $path['filename'],
496
        ];
497 2
    }
498
499 2
    /**
500 2
     * @param string $directory
501 2
     * @param bool   $recursive
502 2
     *
503 2
     * @return mixed
504
     */
505
    private function listObjects($directory = '', $recursive = false)
506
    {
507
        return $this->client->listObjects([
508
            'Bucket'    => $this->getBucket(),
509
            'Prefix'    => ((string) $directory === '') ? '' : ($directory.'/'),
510
            'Delimiter' => $recursive ? '' : '/',
511 4
        ]);
512
    }
513 4
514
    /**
515 4
     * @param Config $config
516
     *
517
     * @return array
518
     */
519 4
    private function prepareUploadConfig(Config $config)
520
    {
521
        $options = [];
522
523 4
        if ($config->has('params')) {
524
            $options['params'] = $config->get('params');
525
        }
526
527
        if ($config->has('visibility')) {
528
            $options['params']['ACL'] = $this->normalizeVisibility($config->get('visibility'));
529
        }
530
531 1
        return $options;
532
    }
533
534 1
    /**
535
     * @param $visibility
536
     *
537
     * @return string
538
     */
539 1
    private function normalizeVisibility($visibility)
540
    {
541
        switch ($visibility) {
542
            case AdapterInterface::VISIBILITY_PUBLIC:
543
                $visibility = 'public-read';
544
                break;
545
        }
546
547
        return $visibility;
548
    }
549
}
550