Passed
Push — master ( 1a1cd5...cc6d2c )
by frey
05:06
created

Adapter::getAuthorization()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 10
ccs 0
cts 7
cp 0
crap 2
rs 10
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 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('%s.cos.%s.myqcloud.com/%s',
106 2
            $this->getBucketWithAppId(), $this->getRegion(), $path
107 2
        );
108
    }
109
110
    /**
111
     * @param $path
112
     *
113
     * @return string
114
     */
115
    public function getPicturePath($path)
116
    {
117
        return sprintf('%s.pic.%s.myqcloud.com/%s',
118
            $this->getBucketWithAppId(), $this->getRegion(), $path
119
        );
120
    }
121
122
    /**
123
     * @param string $path
124
     *
125
     * @return string
126
     */
127 1
    public function getUrl($path)
128
    {
129 1
        if ($this->config['cdn']) {
130
            return $this->applyPathPrefix($path);
131
        }
132
133
        $options = [
134 1
            'Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http',
135 1
        ];
136
137 1
        $objectUrl = $this->client->getObjectUrl(
138 1
            $this->getBucket(), $path, null, $options
139 1
        );
140
141 1
        return $objectUrl;
142
    }
143
144
    /**
145
     * @param string             $path
146
     * @param \DateTimeInterface $expiration
147
     * @param array              $options
148
     *
149
     * @return string
150
     */
151 3
    public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
152
    {
153 2
        $options = array_merge(
154 2
            $options,
155 2
            ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']
156 2
        );
157
158 2
        $objectUrl = $this->client->getObjectUrl(
159 2
            $this->getBucket(), $path, $expiration->format('c'), $options
160 3
        );
161
162 2
        return $objectUrl;
163
    }
164
165
    /**
166
     * @param string $path
167
     * @param string $contents
168
     * @param Config $config
169
     *
170
     * @return array|false
171
     */
172 2
    public function write($path, $contents, Config $config)
173 2
    {
174 2
        $options = $this->prepareUploadConfig($config);
175
176 2
        return $this->client->upload($this->getBucket(), $path, $contents, $options);
177
    }
178
179
    /**
180
     * @param string   $path
181
     * @param resource $resource
182
     * @param Config   $config
183
     *
184
     * @return array|false
185
     */
186 2
    public function writeStream($path, $resource, Config $config)
187
    {
188 2
        $options = $this->prepareUploadConfig($config);
189
190 2
        return $this->client->upload(
191 2
            $this->getBucket(),
192 2
            $path,
193 2
            stream_get_contents($resource, -1, 0),
194
            $options
195 2
        );
196
    }
197
198
    /**
199
     * @param string $path
200
     * @param string $contents
201
     * @param Config $config
202
     *
203
     * @return array|false
204
     */
205 1
    public function update($path, $contents, Config $config)
206
    {
207 1
        return $this->write($path, $contents, $config);
208
    }
209
210
    /**
211
     * @param string   $path
212
     * @param resource $resource
213
     * @param Config   $config
214
     *
215
     * @return array|false
216
     */
217 1
    public function updateStream($path, $resource, Config $config)
218
    {
219 1
        return $this->writeStream($path, $resource, $config);
220
    }
221
222
    /**
223
     * @param string $path
224
     * @param string $newpath
225
     *
226
     * @return bool
227
     */
228 1
    public function rename($path, $newpath)
229
    {
230 1
        $result = $this->copy($path, $newpath);
231
232 1
        $this->delete($path);
233
234 1
        return $result;
235
    }
236
237
    /**
238
     * @param string $path
239
     * @param string $newpath
240
     *
241
     * @return bool
242
     */
243 2
    public function copy($path, $newpath)
244
    {
245 2
        $source = $this->getSourcePath($path);
246
247 2
        return (bool) $this->client->copy($this->getBucket(), $newpath, $source);
248
    }
249
250
    /**
251
     * @param string $path
252
     *
253
     * @return bool
254
     */
255 2
    public function delete($path)
256
    {
257 2
        $result = $this->client->deleteObject([
258 2
            'Bucket' => $this->getBucket(),
259 2
            'Key'    => $path,
260 2
        ]);
261
262 2
        return (bool) $result;
263
    }
264
265
    /**
266
     * @param string $dirname
267
     *
268
     * @return bool
269
     */
270 1
    public function deleteDir($dirname)
271
    {
272 1
        $result = $this->client->deleteObject([
273 1
            'Bucket' => $this->getBucket(),
274 1
            'Key'    => $dirname.'/',
275 1
        ]);
276
277 1
        return (bool) $result;
278
    }
279
280
    /**
281
     * @param string $dirname
282
     * @param Config $config
283
     *
284
     * @return array|false
285
     */
286 1
    public function createDir($dirname, Config $config)
287
    {
288 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|false.
Loading history...
289 1
            'Bucket' => $this->getBucket(),
290 1
            'Key'    => $dirname.'/',
291 1
            'Body'   => '',
292 1
        ]);
293
    }
294
295
    /**
296
     * @param string $path
297
     * @param string $visibility
298
     *
299
     * @return bool
300
     */
301 1
    public function setVisibility($path, $visibility)
302
    {
303 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...
304 1
            'Bucket' => $this->getBucket(),
305 1
            'Key'    => $path,
306 1
            'ACL'    => $this->normalizeVisibility($visibility),
307 1
        ]);
308
    }
309
310
    /**
311
     * @param string $path
312
     *
313
     * @return bool
314
     */
315 1
    public function has($path)
316
    {
317
        try {
318 1
            return (bool) $this->getMetadata($path);
319
        } catch (NoSuchKeyException $e) {
320
            return false;
321
        }
322
    }
323
324
    /**
325
     * @param string $path
326
     *
327
     * @return array|bool
328
     */
329 1
    public function read($path)
330
    {
331
        try {
332 1
            $response = $this->forceReadFromCDN()
333 1
                ? $this->readFromCDN($path)
334 1
                : $this->readFromSource($path);
335
336 1
            return ['contents' => (string) $response];
337
        } catch (NoSuchKeyException $e) {
338
            return false;
339
        } catch (\GuzzleHttp\Exception\ClientException $e) {
340
            return false;
341
        }
342
    }
343
344
    /**
345
     * @return bool
346
     */
347 1
    protected function forceReadFromCDN()
348
    {
349 1
        return $this->config['cdn']
350 1
            && isset($this->config['read_from_cdn'])
351 1
            && $this->config['read_from_cdn'];
352
    }
353
354
    /**
355
     * @param $path
356
     *
357
     * @return string
358
     */
359
    protected function readFromCDN($path)
360
    {
361
        return $this->getHttpClient()
362
            ->get($this->applyPathPrefix($path))
363
            ->getBody()
364
            ->getContents();
365
    }
366
367
    /**
368
     * @param $path
369
     *
370
     * @return string
371
     */
372 1
    protected function readFromSource($path)
373
    {
374 1
        return $this->client->getObject([
375 1
            'Bucket' => $this->getBucket(),
376 1
            'Key'    => $path,
377 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

377
        ])->/** @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...
378
    }
379
380
    /**
381
     * @return \GuzzleHttp\Client
382
     */
383 1
    public function getHttpClient()
384
    {
385 1
        return new \GuzzleHttp\Client([
386 1
            'timeout'         => $this->config['timeout'],
387 1
            'connect_timeout' => $this->config['connect_timeout'],
388 1
        ]);
389
    }
390
391
    /**
392
     * @param string $path
393
     *
394
     * @return array|bool
395
     */
396 1
    public function readStream($path)
397
    {
398
        try {
399 1
            $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
400
401 1
            $stream = $this->getHttpClient()
402 1
                           ->get($temporaryUrl, ['stream' => true])
403 1
                           ->getBody()
404 1
                           ->detach();
405
406 1
            return ['stream' => $stream];
407
        } catch (NoSuchKeyException $e) {
408
            return false;
409
        } catch (\GuzzleHttp\Exception\ClientException $e) {
410
            return false;
411
        }
412
    }
413
414
    /**
415
     * @param string $directory
416
     * @param bool   $recursive
417
     *
418
     * @return array|bool
419
     */
420 1
    public function listContents($directory = '', $recursive = false)
421
    {
422 1
        $list = [];
423
424 1
        $marker = '';
425 1
        while (true) {
426 1
            $response = $this->listObjects($directory, $recursive, $marker);
427
428 1
            foreach ((array) $response->get('Contents') as $content) {
429 1
                $list[] = $this->normalizeFileInfo($content);
430 1
            }
431
432 1
            if (!$response->get('IsTruncated')) {
433 1
                break;
434
            }
435
            $marker = $response->get('NextMarker') ?: '';
436
        }
437
438 1
        return $list;
439
    }
440
441
    /**
442
     * @param string $path
443
     *
444
     * @return array|bool
445
     */
446 5
    public function getMetadata($path)
447
    {
448 5
        return $this->client->headObject([
449 5
            'Bucket' => $this->getBucket(),
450 5
            'Key'    => $path,
451 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

451
        ])->/** @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...
452
    }
453
454
    /**
455
     * @param string $path
456
     *
457
     * @return array|bool
458
     */
459 1
    public function getSize($path)
460
    {
461 1
        $meta = $this->getMetadata($path);
462
463 1
        return isset($meta['ContentLength'])
464 1
            ? ['size' => $meta['ContentLength']] : false;
465
    }
466
467
    /**
468
     * @param string $path
469
     *
470
     * @return array|bool
471
     */
472 1
    public function getMimetype($path)
473
    {
474 1
        $meta = $this->getMetadata($path);
475
476 1
        return isset($meta['ContentType'])
477 1
            ? ['mimetype' => $meta['ContentType']] : false;
478
    }
479
480
    /**
481
     * @param string $path
482
     *
483
     * @return array|bool
484
     */
485 1
    public function getTimestamp($path)
486
    {
487 1
        $meta = $this->getMetadata($path);
488
489 1
        return isset($meta['LastModified'])
490 1
            ? ['timestamp' => strtotime($meta['LastModified'])] : false;
491
    }
492
493
    /**
494
     * @param string $path
495
     *
496
     * @return array|bool
497
     */
498 1
    public function getVisibility($path)
499
    {
500 1
        $meta = $this->client->getObjectAcl([
501 1
            'Bucket' => $this->getBucket(),
502 1
            'Key'    => $path,
503 1
        ]);
504
505 1
        foreach ($meta->get('Grants') as $grant) {
506 1
            if (isset($grant['Grantee']['URI'])
507 1
                && $grant['Permission'] === 'READ'
508 1
                && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
509 1
            ) {
510
                return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
511
            }
512 1
        }
513
514 1
        return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
515
    }
516
517
    /**
518
     * @param array $content
519
     *
520
     * @return array
521
     */
522 1
    private function normalizeFileInfo(array $content)
523
    {
524 1
        $path = pathinfo($content['Key']);
525
526
        return [
527 1
            'type'      => substr($content['Key'], -1) === '/' ? 'dir' : 'file',
528 1
            'path'      => $content['Key'],
529 1
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
530 1
            'size'      => (int) $content['Size'],
531 1
            'dirname'   => (string) $path['dirname'],
532 1
            'basename'  => (string) $path['basename'],
533 1
            'extension' => isset($path['extension']) ? $path['extension'] : '',
534 1
            'filename'  => (string) $path['filename'],
535 1
        ];
536
    }
537
538
    /**
539
     * @param string $directory
540
     * @param bool   $recursive
541
     * @param string $marker    max return 1000 record, if record greater than 1000
542
     *                          you should set the next marker to get the full list
543
     *
544
     * @return mixed
545
     */
546 1
    private function listObjects($directory = '', $recursive = false, $marker = '')
547
    {
548 1
        return $this->client->listObjects([
549 1
            'Bucket'    => $this->getBucket(),
550 1
            'Prefix'    => ((string) $directory === '') ? '' : ($directory.'/'),
551 1
            'Delimiter' => $recursive ? '' : '/',
552 1
            'Marker'    => $marker,
553 1
            'MaxKeys'   => 1000,
554 1
        ]);
555
    }
556
557
    /**
558
     * @param Config $config
559
     *
560
     * @return array
561
     */
562 4
    private function prepareUploadConfig(Config $config)
563
    {
564 4
        $options = [];
565
566 4
        if (isset($this->config['encrypt']) && $this->config['encrypt']) {
567
            $options['params']['ServerSideEncryption'] = 'AES256';
568
        }
569
570 4
        if ($config->has('params')) {
571
            $options['params'] = $config->get('params');
572
        }
573
574 4
        if ($config->has('visibility')) {
575
            $options['params']['ACL'] = $this->normalizeVisibility($config->get('visibility'));
576
        }
577
578 4
        return $options;
579
    }
580
581
    /**
582
     * @param $visibility
583
     *
584
     * @return string
585
     */
586 1
    private function normalizeVisibility($visibility)
587
    {
588
        switch ($visibility) {
589 1
            case AdapterInterface::VISIBILITY_PUBLIC:
590
                $visibility = 'public-read';
591
                break;
592
        }
593
594 1
        return $visibility;
595
    }
596
597
    /**
598
     * @return Client
599
     */
600
    public function getCOSClient()
601
    {
602
        return $this->client;
603
    }
604
605
    /**
606
     * @param $method
607
     * @param $url
608
     *
609
     * @return string
610
     */
611
    public function getAuthorization($method, $url)
612
    {
613
        $cosRequest = new \Guzzle\Http\Message\Request($method, $url);
614
615
        $signature = new \Qcloud\Cos\Signature(
616
            $this->config['credentials']['secretId'],
617
            $this->config['credentials']['secretKey']
618
        );
619
620
        return $signature->createAuthorization($cosRequest);
621
    }
622
}
623