Passed
Pull Request — master (#40)
by frey
03:09
created

Adapter::getUrl()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 3.0067

Importance

Changes 6
Bugs 4 Features 1
Metric Value
cc 3
eloc 10
c 6
b 4
f 1
nc 3
nop 1
dl 0
loc 19
ccs 10
cts 11
cp 0.9091
crap 3.0067
rs 9.9332
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\ServiceResponseException;
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 20
    public function getBucketWithAppId()
65
    {
66 20
        return $this->getBucket().'-'.$this->getAppId();
67
    }
68
69
    /**
70
     * @return string
71
     */
72 20
    public function getBucket()
73
    {
74 20
        return preg_replace(
75 20
            "/-{$this->getAppId()}$/",
76 20
            '',
77 20
            $this->config['bucket']
78 20
        );
79
    }
80
81
    /**
82
     * @return string
83
     */
84 20
    public function getAppId()
85
    {
86 20
        return $this->config['credentials']['appId'];
87
    }
88
89
    /**
90
     * @return string
91
     */
92 2
    public function getRegion()
93
    {
94 2
        return array_key_exists($this->config['region'], $this->regionMap)
95 2
            ? $this->regionMap[$this->config['region']] : $this->config['region'];
96
    }
97
98
    /**
99
     * @param $path
100
     *
101
     * @return string
102
     */
103 2
    public function getSourcePath($path)
104
    {
105 2
        return sprintf(
106 2
            '%s.cos.%s.myqcloud.com/%s',
107 2
            $this->getBucketWithAppId(),
108 2
            $this->getRegion(),
109
            $path
110 2
        );
111
    }
112
113
    /**
114
     * @param $path
115
     *
116
     * @return string
117
     */
118
    public function getPicturePath($path)
119
    {
120
        return sprintf(
121
            '%s.pic.%s.myqcloud.com/%s',
122
            $this->getBucketWithAppId(),
123
            $this->getRegion(),
124
            $path
125
        );
126
    }
127
128
    /**
129
     * @param string $path
130
     *
131
     * @return string
132
     */
133 1
    public function getUrl($path)
134
    {
135 1
        if ($this->config['cdn']) {
136
            return $this->applyPathPrefix($path);
137
        }
138
139
        $options = [
140 1
            'Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http',
141 1
        ];
142
143
        /** @var \GuzzleHttp\Psr7\Uri $objectUrl */
144 1
        $objectUrl = $this->client->getObjectUrl(
145 1
            $this->getBucketWithAppId(),
146 1
            $path,
147 1
            '+30 minutes',
148
            $options
149 1
        );
150
151 1
        return (string) $objectUrl;
152
    }
153
154
    /**
155
     * @param string             $path
156
     * @param \DateTimeInterface $expiration
157
     * @param array              $options
158
     *
159
     * @return string
160
     */
161 2
    public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
162
    {
163 2
        $options = array_merge(
164 2
            $options,
165 2
            ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']
166 2
        );
167
168
        /** @var \GuzzleHttp\Psr7\Uri $objectUrl */
169 2
        $objectUrl = $this->client->getObjectUrl(
170 2
            $this->getBucketWithAppId(),
171 2
            $path,
172 2
            $expiration->format('c'),
173
            $options
174 2
        );
175
176 2
        return (string) $objectUrl;
177
    }
178
179
    /**
180
     * @param string $path
181
     * @param string $contents
182
     * @param Config $config
183
     *
184
     * @return array|false
185
     */
186 2
    public function write($path, $contents, Config $config)
187
    {
188
        try {
189 2
            return $this->client->upload(
190 2
                $this->getBucketWithAppId(),
191 2
                $path,
192 2
                $contents,
193 2
                $this->prepareUploadConfig($config)
194 2
            );
195
        } catch (ServiceResponseException $e) {
196
            return false;
197
        }
198
    }
199
200
    /**
201
     * @param string   $path
202
     * @param resource $resource
203
     * @param Config   $config
204
     *
205
     * @return array|false
206
     */
207 2
    public function writeStream($path, $resource, Config $config)
208
    {
209
        try {
210 2
            return $this->client->upload(
211 2
                $this->getBucketWithAppId(),
212 2
                $path,
213 2
                stream_get_contents($resource, -1, 0),
214 2
                $this->prepareUploadConfig($config)
215 2
            );
216
        } catch (ServiceResponseException $e) {
217
            return false;
218
        }
219
    }
220
221
    /**
222
     * @param string $path
223
     * @param string $contents
224
     * @param Config $config
225
     *
226
     * @return array|false
227
     */
228 1
    public function update($path, $contents, Config $config)
229
    {
230 1
        return $this->write($path, $contents, $config);
231
    }
232
233
    /**
234
     * @param string   $path
235
     * @param resource $resource
236
     * @param Config   $config
237
     *
238
     * @return array|false
239
     */
240 1
    public function updateStream($path, $resource, Config $config)
241
    {
242 1
        return $this->writeStream($path, $resource, $config);
243
    }
244
245
    /**
246
     * @param string $path
247
     * @param string $newpath
248
     *
249
     * @return bool
250
     */
251 1
    public function rename($path, $newpath)
252
    {
253
        try {
254 1
            if ($result = $this->copy($path, $newpath)) {
255 1
                $this->delete($path);
256 1
            }
257
258 1
            return $result;
259
        } catch (ServiceResponseException $e) {
260
            return false;
261
        }
262
    }
263
264
    /**
265
     * @param string $path
266
     * @param string $newpath
267
     *
268
     * @return bool
269
     */
270 2
    public function copy($path, $newpath)
271
    {
272
        try {
273 2
            return (bool) $this->client->copyObject([
274 2
                'Bucket'     => $this->getBucketWithAppId(),
275 2
                'Key'        => $newpath,
276 2
                'CopySource' => $this->getSourcePath($path),
277 2
            ]);
278
        } catch (ServiceResponseException $e) {
279
            return false;
280
        }
281
    }
282
283
    /**
284
     * @param string $path
285
     *
286
     * @return bool
287
     */
288 2
    public function delete($path)
289
    {
290
        try {
291 2
            return (bool) $this->client->deleteObject([
292 2
                'Bucket' => $this->getBucketWithAppId(),
293 2
                'Key'    => $path,
294 2
            ]);
295
        } catch (ServiceResponseException $e) {
296
            return false;
297
        }
298
    }
299
300
    /**
301
     * @param string $dirname
302
     *
303
     * @return bool
304
     */
305 1
    public function deleteDir($dirname)
306
    {
307
        try {
308 1
            return (bool) $this->client->deleteObject([
309 1
                'Bucket' => $this->getBucketWithAppId(),
310 1
                'Key'    => $dirname.'/',
311 1
            ]);
312
        } catch (ServiceResponseException $e) {
313
            return false;
314
        }
315
    }
316
317
    /**
318
     * @param string $dirname
319
     * @param Config $config
320
     *
321
     * @return array|false
322
     */
323 1
    public function createDir($dirname, Config $config)
324
    {
325
        try {
326 1
            return $this->client->putObject([
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->client->pu...e . '/', 'Body' => '')) also could return the type GuzzleHttp\Command\Resul...romise\PromiseInterface which is incompatible with the documented return type array|false.
Loading history...
327 1
                'Bucket' => $this->getBucketWithAppId(),
328 1
                'Key'    => $dirname.'/',
329 1
                'Body'   => '',
330 1
            ]);
331
        } catch (ServiceResponseException $e) {
332
            return false;
333
        }
334
    }
335
336
    /**
337
     * @param string $path
338
     * @param string $visibility
339
     *
340
     * @return bool
341
     */
342 1
    public function setVisibility($path, $visibility)
343
    {
344
        try {
345 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...
346 1
                'Bucket' => $this->getBucketWithAppId(),
347 1
                'Key'    => $path,
348 1
                'ACL'    => $this->normalizeVisibility($visibility),
349 1
            ]);
350
        } catch (ServiceResponseException $e) {
351
            return false;
352
        }
353
    }
354
355
    /**
356
     * @param string $path
357
     *
358
     * @return bool
359
     */
360 1
    public function has($path)
361
    {
362
        try {
363 1
            return (bool) $this->getMetadata($path);
364
        } catch (ServiceResponseException $e) {
365
            return false;
366
        }
367
    }
368
369
    /**
370
     * @param string $path
371
     *
372
     * @return array|bool
373
     */
374 1
    public function read($path)
375
    {
376
        try {
377 1
            $response = $this->forceReadFromCDN()
378 1
                ? $this->readFromCDN($path)
379 1
                : $this->readFromSource($path);
380
381 1
            return ['contents' => (string) $response];
382
        } catch (ServiceResponseException $e) {
383
            return false;
384
        }
385
    }
386
387
    /**
388
     * @return bool
389
     */
390 1
    protected function forceReadFromCDN()
391
    {
392 1
        return $this->config['cdn']
393 1
            && isset($this->config['read_from_cdn'])
394 1
            && $this->config['read_from_cdn'];
395
    }
396
397
    /**
398
     * @param $path
399
     *
400
     * @return string
401
     */
402
    protected function readFromCDN($path)
403
    {
404
        return $this->getHttpClient()
405
            ->get($this->applyPathPrefix($path))
406
            ->getBody()
407
            ->getContents();
408
    }
409
410
    /**
411
     * @param $path
412
     *
413
     * @return string
414
     */
415 1
    protected function readFromSource($path)
416
    {
417
        try {
418 1
            $response = $this->client->getObject([
419 1
                'Bucket' => $this->getBucketWithAppId(),
420 1
                'Key'    => $path,
421 1
            ]);
422
423 1
            return $response['Body'];
424
        } catch (ServiceResponseException $e) {
425
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type string.
Loading history...
426
        }
427
    }
428
429
    /**
430
     * @return \GuzzleHttp\Client
431
     */
432 1
    public function getHttpClient()
433
    {
434 1
        return new \GuzzleHttp\Client([
435 1
            'timeout'         => $this->config['timeout'],
436 1
            'connect_timeout' => $this->config['connect_timeout'],
437 1
        ]);
438
    }
439
440
    /**
441
     * @param string $path
442
     *
443
     * @return array|bool
444
     */
445 1
    public function readStream($path)
446
    {
447
        try {
448 1
            $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
449
450 1
            $stream = $this->getHttpClient()
451 1
                           ->get($temporaryUrl, ['stream' => true])
452 1
                           ->getBody()
453 1
                           ->detach();
454
455 1
            return ['stream' => $stream];
456
        } catch (ServiceResponseException $e) {
457
            return false;
458
        }
459
    }
460
461
    /**
462
     * @param string $directory
463
     * @param bool   $recursive
464
     *
465
     * @return array|bool
466
     */
467 1
    public function listContents($directory = '', $recursive = false)
468
    {
469 1
        $list = [];
470
471 1
        $marker = '';
472 1
        while (true) {
473 1
            $response = $this->listObjects($directory, $recursive, $marker);
474
475 1
            foreach ((array) $response['Contents'] as $content) {
476 1
                $list[] = $this->normalizeFileInfo($content);
477 1
            }
478
479 1
            if (!$response['IsTruncated']) {
480 1
                break;
481
            }
482
            $marker = $response['NextMarker'] ?: '';
483
        }
484
485 1
        return $list;
486
    }
487
488
    /**
489
     * @param string $path
490
     *
491
     * @return array|bool
492
     */
493 5
    public function getMetadata($path)
494
    {
495
        try {
496 5
            return $this->client->headObject([
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->client->he...pId(), 'Key' => $path)) also could return the type GuzzleHttp\Command\Resul...romise\PromiseInterface which is incompatible with the documented return type array|boolean.
Loading history...
497 5
                'Bucket' => $this->getBucketWithAppId(),
498 5
                'Key'    => $path,
499 5
            ]);
500
        } catch (ServiceResponseException $e) {
501
            return false;
502
        }
503
    }
504
505
    /**
506
     * @param string $path
507
     *
508
     * @return array|bool
509
     */
510 1
    public function getSize($path)
511
    {
512 1
        $meta = $this->getMetadata($path);
513
514 1
        return isset($meta['ContentLength'])
515 1
            ? ['size' => $meta['ContentLength']] : false;
516
    }
517
518
    /**
519
     * @param string $path
520
     *
521
     * @return array|bool
522
     */
523 1
    public function getMimetype($path)
524
    {
525 1
        $meta = $this->getMetadata($path);
526
527 1
        return isset($meta['ContentType'])
528 1
            ? ['mimetype' => $meta['ContentType']] : false;
529
    }
530
531
    /**
532
     * @param string $path
533
     *
534
     * @return array|bool
535
     */
536 1
    public function getTimestamp($path)
537
    {
538 1
        $meta = $this->getMetadata($path);
539
540 1
        return isset($meta['LastModified'])
541 1
            ? ['timestamp' => strtotime($meta['LastModified'])] : false;
542
    }
543
544
    /**
545
     * @param string $path
546
     *
547
     * @return array|bool
548
     */
549 1
    public function getVisibility($path)
550
    {
551
        try {
552 1
            $meta = $this->client->getObjectAcl([
553 1
                'Bucket' => $this->getBucketWithAppId(),
554 1
                'Key'    => $path,
555 1
            ]);
556
557 1
            foreach ($meta['Grants'] as $grant) {
558 1
                if (isset($grant['Grantee']['URI'])
559 1
                    && $grant['Permission'] === 'READ'
560 1
                    && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
561 1
                ) {
562
                    return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
563
                }
564 1
            }
565
566 1
            return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
567
        } catch (ServiceResponseException $e) {
568
            return false;
569
        }
570
    }
571
572
    /**
573
     * @param array $content
574
     *
575
     * @return array
576
     */
577 1
    private function normalizeFileInfo(array $content)
578
    {
579 1
        $path = pathinfo($content['Key']);
580
581
        return [
582 1
            'type'      => substr($content['Key'], -1) === '/' ? 'dir' : 'file',
583 1
            'path'      => $content['Key'],
584 1
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
585 1
            'size'      => (int) $content['Size'],
586 1
            'dirname'   => $path['dirname'] === '.' ? '' : (string) $path['dirname'],
587 1
            'basename'  => (string) $path['basename'],
588 1
            'extension' => isset($path['extension']) ? $path['extension'] : '',
589 1
            'filename'  => (string) $path['filename'],
590 1
        ];
591
    }
592
593
    /**
594
     * @param string $directory
595
     * @param bool   $recursive
596
     * @param string $marker    max return 1000 record, if record greater than 1000
597
     *                          you should set the next marker to get the full list
598
     *
599
     * @return \GuzzleHttp\Command\Result|array
600
     */
601 1
    private function listObjects($directory = '', $recursive = false, $marker = '')
602
    {
603
        try {
604 1
            return $this->client->listObjects([
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->client->li...er, 'MaxKeys' => 1000)) also could return the type GuzzleHttp\Promise\PromiseInterface which is incompatible with the documented return type GuzzleHttp\Command\Result|array.
Loading history...
605 1
                'Bucket'    => $this->getBucketWithAppId(),
606 1
                'Prefix'    => ((string) $directory === '') ? '' : ($directory.'/'),
607 1
                'Delimiter' => $recursive ? '' : '/',
608 1
                'Marker'    => $marker,
609 1
                'MaxKeys'   => 1000,
610 1
            ]);
611
        } catch (ServiceResponseException $e) {
612
            return [
613
                'Contents'    => [],
614
                'IsTruncated' => false,
615
                'NextMarker'  => '',
616
            ];
617
        }
618
    }
619
620
    /**
621
     * @param Config $config
622
     *
623
     * @return array
624
     */
625 4
    private function prepareUploadConfig(Config $config)
626
    {
627 4
        $options = [];
628
629 4
        if (isset($this->config['encrypt']) && $this->config['encrypt']) {
630
            $options['ServerSideEncryption'] = 'AES256';
631
        }
632
633 4
        if ($config->has('params')) {
634
            $options = array_merge($options, $config->get('params'));
635
        }
636
637 4
        if ($config->has('visibility')) {
638
            $options['ACL'] = $this->normalizeVisibility($config->get('visibility'));
639
        }
640
641 4
        return $options;
642
    }
643
644
    /**
645
     * @param $visibility
646
     *
647
     * @return string
648
     */
649 1
    private function normalizeVisibility($visibility)
650
    {
651
        switch ($visibility) {
652 1
            case AdapterInterface::VISIBILITY_PUBLIC:
653
                $visibility = 'public-read';
654
                break;
655
        }
656
657 1
        return $visibility;
658
    }
659
660
    /**
661
     * @return Client
662
     */
663
    public function getCOSClient()
664
    {
665
        return $this->client;
666
    }
667
668
    /**
669
     * @param $method
670
     * @param $url
671
     *
672
     * @return string
673
     */
674
    public function getAuthorization($method, $url)
675
    {
676
        $cosRequest = new \GuzzleHttp\Psr7\Request($method, $url);
677
678
        $signature = new \Qcloud\Cos\Signature(
679
            $this->config['credentials']['secretId'],
680
            $this->config['credentials']['secretKey']
681
        );
682
683
        return $signature->createAuthorization($cosRequest);
684
    }
685
}
686