Completed
Push — master ( 1bc6b8...73dec7 )
by frey
04:39
created

Adapter::forceReadFromCDN()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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

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

450
        ])->/** @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...
451
    }
452
453
    /**
454
     * @param string $path
455
     *
456
     * @return array|bool
457
     */
458 1
    public function getSize($path)
459
    {
460 1
        $meta = $this->getMetadata($path);
461
462 1
        return isset($meta['ContentLength'])
463 1
            ? ['size' => $meta['ContentLength']] : false;
464
    }
465
466
    /**
467
     * @param string $path
468
     *
469
     * @return array|bool
470
     */
471 1
    public function getMimetype($path)
472
    {
473 1
        $meta = $this->getMetadata($path);
474
475 1
        return isset($meta['ContentType'])
476 1
            ? ['mimetype' => $meta['ContentType']] : false;
477
    }
478
479
    /**
480
     * @param string $path
481
     *
482
     * @return array|bool
483
     */
484 1
    public function getTimestamp($path)
485
    {
486 1
        $meta = $this->getMetadata($path);
487
488 1
        return isset($meta['LastModified'])
489 1
            ? ['timestamp' => strtotime($meta['LastModified'])] : false;
490
    }
491
492
    /**
493
     * @param string $path
494
     *
495
     * @return array|bool
496
     */
497 1
    public function getVisibility($path)
498
    {
499 1
        $meta = $this->client->getObjectAcl([
500 1
            'Bucket' => $this->getBucket(),
501 1
            'Key'    => $path,
502 1
        ]);
503
504 1
        foreach ($meta->get('Grants') as $grant) {
505 1
            if (isset($grant['Grantee']['URI'])
506 1
                && $grant['Permission'] === 'READ'
507 1
                && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
508 1
            ) {
509
                return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
510
            }
511 1
        }
512
513 1
        return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
514
    }
515
516
    /**
517
     * @param array $content
518
     *
519
     * @return array
520
     */
521 1
    private function normalizeFileInfo(array $content)
522
    {
523 1
        $path = pathinfo($content['Key']);
524
525
        return [
526 1
            'type'      => substr($content['Key'], -1) === '/' ? 'dir' : 'file',
527 1
            'path'      => $content['Key'],
528 1
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
529 1
            'size'      => (int) $content['Size'],
530 1
            'dirname'   => (string) $path['dirname'],
531 1
            'basename'  => (string) $path['basename'],
532 1
            'extension' => isset($path['extension']) ? $path['extension'] : '',
533 1
            'filename'  => (string) $path['filename'],
534 1
        ];
535
    }
536
537
    /**
538
     * @param string $directory
539
     * @param bool   $recursive
540
     *
541
     * @return mixed
542
     */
543 2
    private function listObjects($directory = '', $recursive = false)
544
    {
545 2
        return $this->client->listObjects([
546 2
            'Bucket'    => $this->getBucket(),
547 2
            'Prefix'    => ((string) $directory === '') ? '' : ($directory.'/'),
548 2
            'Delimiter' => $recursive ? '' : '/',
549 2
        ]);
550
    }
551
552
    /**
553
     * @param Config $config
554
     *
555
     * @return array
556
     */
557 4
    private function prepareUploadConfig(Config $config)
558
    {
559 4
        $options = [];
560
561 4
        if ($config->has('params')) {
562
            $options['params'] = $config->get('params');
563
        }
564
565 4
        if ($config->has('visibility')) {
566
            $options['params']['ACL'] = $this->normalizeVisibility($config->get('visibility'));
567
        }
568
569 4
        return $options;
570
    }
571
572
    /**
573
     * @param $visibility
574
     *
575
     * @return string
576
     */
577 1
    private function normalizeVisibility($visibility)
578
    {
579
        switch ($visibility) {
580 1
            case AdapterInterface::VISIBILITY_PUBLIC:
581
                $visibility = 'public-read';
582
                break;
583
        }
584
585 1
        return $visibility;
586
    }
587
}
588