Passed
Push — master ( 7334f7...9302af )
by frey
07:58
created

Adapter::normalizeVisibility()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

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

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

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