Test Failed
Push — master ( 1bb225...f26325 )
by frey
02:58
created

Adapter   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 538
Duplicated Lines 0 %

Test Coverage

Coverage 81.35%

Importance

Changes 0
Metric Value
eloc 169
dl 0
loc 538
ccs 157
cts 193
cp 0.8135
rs 4.08
c 0
b 0
f 0
wmc 59

32 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A getBucketWithAppId() 0 3 1
A getAppId() 0 3 1
A getBucket() 0 6 1
A getSourcePath() 0 4 1
A getRegion() 0 4 2
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 getHttpClient() 0 5 1
A delete() 0 5 1
A normalizeFileInfo() 0 13 3
A readStream() 0 15 3
A has() 0 6 2
A read() 0 20 5
A deleteDir() 0 15 2
A getTemporaryUrl() 0 16 2
A listContents() 0 11 2
A update() 0 3 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 getUrl() 0 19 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 2
    public function getBucketWithAppId()
65 1
    {
66 2
        return $this->getBucket().'-'.$this->getAppId();
67
    }
68
69
    /**
70
     * @return string
71
     */
72 19
    public function getBucket()
73
    {
74 19
        return preg_replace(
75
            "/-{$this->getAppId()}$/",
76
            '',
77
            $this->config['bucket']
78
        );
79
    }
80 19
81
    /**
82 19
     * @return string
83
     */
84
    public function getAppId()
85
    {
86
        return $this->config['credentials']['appId'];
87
    }
88 2
89
    /**
90 2
     * @return string
91 2
     */
92
    public function getRegion()
93
    {
94
        return array_key_exists($this->config['region'], $this->regionMap)
95
            ? $this->regionMap[$this->config['region']] : $this->config['region'];
96
    }
97
98
    /**
99 2
     * @param $path
100
     *
101 2
     * @return string
102 2
     */
103 2
    public function getSourcePath($path)
104
    {
105
        return sprintf('%s.cos.%s.myqcloud.com/%s',
106
            $this->getBucketWithAppId(), $this->getRegion(), $path
107
        );
108
    }
109
110
    /**
111 3
     * @param string $path
112
     *
113 3
     * @return string
114 3
     */
115
    public function getUrl($path)
116
    {
117
        if ($this->config['cdn']) {
118
            return $this->applyPathPrefix($path);
119
        }
120
121
        $options = [
122
            'Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http'
123
        ];
124
125
        $objectUrl = $this->client->getObjectUrl(
126
            $this->getBucket(), $path, null, $options
127
        );
128
129
        $url = parse_url($objectUrl);
130
131
        return sprintf(
132
            '%s://%s%s',
133
            $url['scheme'], $url['host'], rawurldecode($url['path'])
134
        );
135
    }
136
137 5
    /**
138
     * @param string             $path
139 1
     * @param \DateTimeInterface $expiration
140 1
     * @param array              $options
141
     *
142 1
     * @return string
143 5
     */
144 1
    public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
145
    {
146 5
        $options = array_merge(
147
            $options,
148 1
            ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']
149 1
        );
150
151
        $objectUrl = $this->client->getObjectUrl(
152
            $this->getBucket(), $path, $expiration->format('c'), $options
153
        );
154
155
        $url = parse_url($objectUrl);
156
157
        return sprintf(
158
            '%s://%s%s?%s',
159
            $url['scheme'], $url['host'], rawurldecode($url['path']), $url['query']
160
        );
161
    }
162 2
163
    /**
164 2
     * @param string $path
165
     * @param string $contents
166 2
     * @param Config $config
167
     *
168
     * @return array|false
169
     */
170
    public function write($path, $contents, Config $config)
171
    {
172
        $options = $this->prepareUploadConfig($config);
173
174
        return $this->client->upload($this->getBucket(), $path, $contents, $options);
175
    }
176 2
177
    /**
178 2
     * @param string   $path
179
     * @param resource $resource
180 2
     * @param Config   $config
181 2
     *
182 2
     * @return array|false
183 2
     */
184
    public function writeStream($path, $resource, Config $config)
185 2
    {
186
        $options = $this->prepareUploadConfig($config);
187
188
        return $this->client->upload(
189
            $this->getBucket(),
190
            $path,
191
            stream_get_contents($resource, -1, 0),
192
            $options
193
        );
194
    }
195 1
196
    /**
197 1
     * @param string $path
198
     * @param string $contents
199
     * @param Config $config
200
     *
201
     * @return array|false
202
     */
203
    public function update($path, $contents, Config $config)
204
    {
205
        return $this->write($path, $contents, $config);
206
    }
207 3
208
    /**
209 3
     * @param string   $path
210
     * @param resource $resource
211
     * @param Config   $config
212
     *
213
     * @return array|false
214
     */
215
    public function updateStream($path, $resource, Config $config)
216
    {
217
        return $this->writeStream($path, $resource, $config);
218 1
    }
219
220 1
    /**
221
     * @param string $path
222 1
     * @param string $newpath
223
     *
224 1
     * @return bool
225
     */
226
    public function rename($path, $newpath)
227
    {
228
        $result = $this->copy($path, $newpath);
229
230
        $this->delete($path);
231
232
        return $result;
233 2
    }
234
235 2
    /**
236
     * @param string $path
237 2
     * @param string $newpath
238
     *
239
     * @return bool
240
     */
241
    public function copy($path, $newpath)
242
    {
243
        $source = $this->getSourcePath($path);
244
245 2
        return (bool) $this->client->copy($this->getBucket(), $newpath, $source);
246
    }
247 2
248 2
    /**
249 2
     * @param string $path
250 2
     *
251
     * @return bool
252
     */
253
    public function delete($path)
254
    {
255
        return (bool) $this->client->deleteObject([
256
            'Bucket' => $this->getBucket(),
257
            'Key'    => $path,
258 1
        ]);
259
    }
260 1
261
    /**
262 1
     * @param string $dirname
263
     *
264
     * @return bool
265
     */
266 1
    public function deleteDir($dirname)
267 1
    {
268 1
        $response = $this->listObjects($dirname);
269
270 1
        if (!isset($response['Contents'])) {
271 1
            return true;
272 1
        }
273 1
274
        $keys = array_map(function ($item) {
275
            return ['Key' => $item['Key']];
276
        }, (array) $response['Contents']);
277
278
        return (bool) $this->client->deleteObjects([
279
            'Bucket'  => $this->getBucket(),
280
            'Objects' => $keys,
281
        ]);
282 1
    }
283
284 1
    /**
285 1
     * @param string $dirname
286 1
     * @param Config $config
287 1
     *
288 1
     * @return array|false
289
     */
290
    public function createDir($dirname, Config $config)
291
    {
292
        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|false.
Loading history...
293
            'Bucket' => $this->getBucket(),
294
            'Key'    => $dirname.'/',
295
            'Body'   => '',
296
        ]);
297 1
    }
298
299 1
    /**
300 1
     * @param string $path
301 1
     * @param string $visibility
302 1
     *
303 1
     * @return bool
304
     */
305
    public function setVisibility($path, $visibility)
306
    {
307
        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...
308
            'Bucket' => $this->getBucket(),
309
            'Key'    => $path,
310
            'ACL'    => $this->normalizeVisibility($visibility),
311 1
        ]);
312
    }
313
314 1
    /**
315
     * @param string $path
316
     *
317
     * @return bool
318
     */
319
    public function has($path)
320
    {
321
        try {
322
            return (bool) $this->getMetadata($path);
323
        } catch (NoSuchKeyException $e) {
324
            return false;
325 1
        }
326
    }
327
328 1
    /**
329
     * @param string $path
330
     *
331
     * @return array|bool
332
     */
333
    public function read($path)
334 1
    {
335 1
        try {
336 1
            if (isset($this->config['read_from_cdn']) && $this->config['read_from_cdn']) {
337 1
                $response = $this->getHttpClient()
338
                                 ->get($this->getTemporaryUrl($path, Carbon::now()->addMinutes(5)))
339
                                 ->getBody()
340 1
                                 ->getContents();
341
            } else {
342
                $response = $this->client->getObject([
343
                    'Bucket' => $this->getBucket(),
344
                    'Key'    => $path,
345
                ])->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

345
                ])->/** @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...
346
            }
347
348
            return ['contents' => (string)$response];
349
        } catch (NoSuchKeyException $e) {
350
            return false;
351 1
        } catch (\GuzzleHttp\Exception\ClientException $e) {
352
            return false;
353 1
        }
354 1
    }
355 1
356 1
    /**
357 1
     * @return \GuzzleHttp\Client
358
     */
359
    public function getHttpClient()
360
    {
361
        return new \GuzzleHttp\Client([
362
            'timeout'         => $this->config['timeout'],
363
            'connect_timeout' => $this->config['connect_timeout'],
364
        ]);
365 1
    }
366
367
    /**
368 1
     * @param string $path
369
     *
370 1
     * @return array|bool
371 1
     */
372 1
    public function readStream($path)
373 1
    {
374
        try {
375 1
            $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
376
377
            $stream = $this->getHttpClient()
378
                           ->get($temporaryUrl, ['stream' => true])
379
                           ->getBody()
380
                           ->detach();
381
382
            return ['stream' => $stream];
383
        } catch (NoSuchKeyException $e) {
384
            return false;
385
        } catch (\GuzzleHttp\Exception\ClientException $e) {
386
            return false;
387
        }
388
    }
389 1
390
    /**
391 1
     * @param string $directory
392
     * @param bool   $recursive
393 1
     *
394
     * @return array|bool
395 1
     */
396 1
    public function listContents($directory = '', $recursive = false)
397 1
    {
398
        $list = [];
399 1
400
        $response = $this->listObjects($directory, $recursive);
401
402
        foreach ((array) $response->get('Contents') as $content) {
403
            $list[] = $this->normalizeFileInfo($content);
404
        }
405
406
        return $list;
407 5
    }
408
409 5
    /**
410 5
     * @param string $path
411 5
     *
412 5
     * @return array|bool
413
     */
414
    public function getMetadata($path)
415
    {
416
        return $this->client->headObject([
417
            'Bucket' => $this->getBucket(),
418
            'Key'    => $path,
419
        ])->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

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