Completed
Push — master ( 5cc937...1bc6b8 )
by frey
05:07
created

Adapter::getBucketWithAppId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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

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

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