Passed
Push — master ( 71d4f9...7f78fc )
by frey
02:16
created

Adapter::listObjects()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 6
cts 6
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 1
nop 2
crap 3
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 2
        if ($this->config['cdn']) {
106 2
            return $this->applyPathPrefix($path);
107
        }
108
109
        $objectUrl = $this->client->getObjectUrl(
110 4
            $this->getBucket(), $path, null,
111
                ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']
112 4
        );
113
114
        $url = parse_url($objectUrl);
115
116
        return sprintf('%s://%s%s', $url['scheme'], $url['host'], urldecode($url['path']));
117
    }
118
119
    /**
120
     * @param string             $path
121
     * @param \DateTimeInterface $expiration
122
     * @param array              $options
123
     *
124
     * @return string
125
     */
126 1
    public function getTemporaryUrl($path, DateTimeInterface $expiration, array $options = [])
127
    {
128 1
        $options = array_merge($options,
129 1
            ['Scheme' => isset($this->config['scheme']) ? $this->config['scheme'] : 'http']);
130
131 1
        $objectUrl = $this->client->getObjectUrl(
132 1
            $this->getBucket(), $path, $expiration->format('c'), $options
133 1
        );
134
135 1
        $url = parse_url($objectUrl);
136
137 1
        if ($this->config['cdn']) {
138 1
            return $this->config['cdn'].urldecode($url['path']).'?'.$url['query'];
139
        }
140
141
        return sprintf('%s://%s%s?%s', $url['scheme'], $url['host'], urldecode($url['path']), $url['query']);
142
    }
143
144
    /**
145
     * @param string $path
146
     * @param string $contents
147
     * @param Config $config
148
     *
149
     * @return array|bool
150
     */
151 2
    public function write($path, $contents, Config $config)
152
    {
153 2
        return $this->client->upload($this->getBucket(), $path, $contents);
154
    }
155
156
    /**
157
     * @param string   $path
158
     * @param resource $resource
159
     * @param Config   $config
160
     *
161
     * @return array|bool
162
     */
163 2
    public function writeStream($path, $resource, Config $config)
164
    {
165 2
        return $this->client->upload($this->getBucket(), $path, stream_get_contents($resource, -1, 0));
166
    }
167
168
    /**
169
     * @param string $path
170
     * @param string $contents
171
     * @param Config $config
172
     *
173
     * @return array|bool
174
     */
175 2
    public function update($path, $contents, Config $config)
176 2
    {
177 1
        return $this->write($path, $contents, $config);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->write($path, $contents, $config); of type array|boolean adds the type boolean to the return on line 177 which is incompatible with the return type declared by the interface League\Flysystem\AdapterInterface::update of type array|false.
Loading history...
178
    }
179
180
    /**
181
     * @param string   $path
182
     * @param resource $resource
183
     * @param Config   $config
184
     *
185
     * @return array|bool
186
     */
187 1
    public function updateStream($path, $resource, Config $config)
188
    {
189 1
        return $this->writeStream($path, $resource, $config);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->writeStream($path, $resource, $config); of type array|boolean adds the type boolean to the return on line 189 which is incompatible with the return type declared by the interface League\Flysystem\AdapterInterface::updateStream of type array|false.
Loading history...
190
    }
191
192
    /**
193
     * @param string $path
194
     * @param string $newpath
195
     *
196
     * @return bool
197
     */
198 1
    public function rename($path, $newpath)
199
    {
200 1
        $result = $this->copy($path, $newpath);
201
202 1
        $this->delete($path);
203
204 1
        return $result;
205
    }
206
207
    /**
208
     * @param string $path
209
     * @param string $newpath
210
     *
211
     * @return bool
212
     */
213 2
    public function copy($path, $newpath)
214
    {
215 2
        $source = $this->getSourcePath($path);
216
217 2
        return (bool) $this->client->copy($this->getBucket(), $newpath, $source);
218
    }
219
220
    /**
221
     * @param string $path
222
     *
223
     * @return bool
224
     */
225 2
    public function delete($path)
226
    {
227 2
        return (bool) $this->client->deleteObject([
228 2
            'Bucket' => $this->getBucket(),
229 2
            'Key'    => $path,
230 2
        ]);
231
    }
232
233
    /**
234
     * @param string $dirname
235
     *
236
     * @return bool
237
     */
238 1
    public function deleteDir($dirname)
239
    {
240 1
        $response = $this->listObjects($dirname);
241
242 1
        if (!isset($response['Contents'])) {
243
            return true;
244
        }
245
246 1
        $keys = array_map(function ($item) {
247 1
            return ['Key' => $item['Key']];
248 1
        }, (array) $response['Contents']);
249
250 1
        return (bool) $this->client->deleteObjects([
251 1
            'Bucket'  => $this->getBucket(),
252 1
            'Objects' => $keys,
253 1
        ]);
254
    }
255
256
    /**
257
     * @param string $dirname
258
     * @param Config $config
259
     *
260
     * @return array|bool
261
     */
262 1
    public function createDir($dirname, Config $config)
263
    {
264 1
        return $this->client->putObject([
265 1
            'Bucket' => $this->getBucket(),
266 1
            'Key'    => $dirname.'/_blank',
267 1
            'Body'   => '',
268 1
        ]);
269
    }
270
271
    /**
272
     * @param string $path
273
     * @param string $visibility
274
     *
275
     * @return bool
276
     */
277 1
    public function setVisibility($path, $visibility)
278
    {
279 1
        $visibility = ($visibility === AdapterInterface::VISIBILITY_PUBLIC)
280 1
            ? 'public-read' : 'private';
281
282 1
        return (bool) $this->client->PutObjectAcl([
0 ignored issues
show
Bug Best Practice introduced by
The return type of return (bool) $this->cli...'ACL' => $visibility)); (boolean) is incompatible with the return type declared by the interface League\Flysystem\AdapterInterface::setVisibility of type array|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
283 1
            'Bucket' => $this->getBucket(),
284 1
            'Key'    => $path,
285 1
            'ACL'    => $visibility,
286 1
        ]);
287
    }
288
289
    /**
290
     * @param string $path
291
     *
292
     * @return bool
293
     */
294 1
    public function has($path)
295
    {
296
        try {
297 1
            return (bool) $this->getMetadata($path);
298
        } catch (NoSuchKeyException $e) {
299
            return false;
300
        }
301
    }
302
303
    /**
304
     * @param string $path
305
     *
306
     * @return array|bool
307
     */
308 1
    public function read($path)
309
    {
310
        try {
311 1
            if (isset($this->config['read_from_cdn']) && $this->config['read_from_cdn']) {
312
                $response = $this->getHttpClient()
313
                                 ->get($this->getTemporaryUrl($path, Carbon::now()->addMinutes(5)))
314
                                 ->getBody()
315
                                 ->getContents();
316
            } else {
317 1
                $response = $this->client->getObject([
0 ignored issues
show
Bug introduced by
The method getObject() does not exist on Qcloud\Cos\Client. Did you maybe mean getObjectUrl()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
318 1
                    'Bucket' => $this->getBucket(),
319 1
                    'Key'    => $path,
320 1
                ])->get('Body');
321
            }
322
323 1
            return ['contents' => (string)$response];
324
        } catch (NoSuchKeyException $e) {
325
            return false;
326
        } catch (\GuzzleHttp\Exception\ClientException $e) {
327
            return false;
328
        }
329
    }
330
331
    /**
332
     * @return \GuzzleHttp\Client
333
     */
334 1
    protected function getHttpClient()
335
    {
336 1
        return $client = (new \GuzzleHttp\Client([
0 ignored issues
show
Unused Code introduced by
$client is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
337 1
            'verify'          => false,
338 1
            'timeout'         => $this->config['timeout'],
339 1
            'connect_timeout' => $this->config['connect_timeout'],
340 1
        ]));
341
    }
342
343
    /**
344
     * @param string $path
345
     *
346
     * @return array|bool
347
     */
348 1
    public function readStream($path)
349
    {
350
        try {
351 1
            $temporaryUrl = $this->getTemporaryUrl($path, Carbon::now()->addMinutes(5));
352
353 1
            $stream = $this->getHttpClient()
354 1
                           ->get($temporaryUrl, ['stream' => true])
355 1
                           ->getBody()
356 1
                           ->detach();
357
358 1
            return ['stream' => $stream];
359
        } catch (NoSuchKeyException $e) {
360
            return false;
361
        } catch (\GuzzleHttp\Exception\ClientException $e) {
362
            return false;
363
        }
364
    }
365
366
    /**
367
     * @param string $directory
368
     * @param bool   $recursive
369
     *
370
     * @return array|bool
371
     */
372 1
    public function listContents($directory = '', $recursive = false)
373
    {
374 1
        $list = [];
375
376 1
        $response = $this->listObjects($directory, $recursive);
377
378 1
        foreach ((array) $response->get('Contents') as $content) {
379 1
            $list[] = $this->normalizeFileInfo($content);
380 1
        }
381
382 1
        return $list;
383
    }
384
385
    /**
386
     * @param string $path
387
     *
388
     * @return array|bool
389
     */
390 5
    public function getMetadata($path)
391
    {
392 5
        return $this->client->headObject([
393 5
            'Bucket' => $this->getBucket(),
394 5
            'Key'    => $path,
395 5
        ])->toArray();
396
    }
397
398
    /**
399
     * @param string $path
400
     *
401
     * @return array|bool
402
     */
403 1
    public function getSize($path)
404
    {
405 1
        $meta = $this->getMetadata($path);
406
407 1
        return isset($meta['ContentLength'])
408 1
            ? ['size' => $meta['ContentLength']] : false;
409
    }
410
411
    /**
412
     * @param string $path
413
     *
414
     * @return array|bool
415
     */
416 1
    public function getMimetype($path)
417
    {
418 1
        $meta = $this->getMetadata($path);
419
420 1
        return isset($meta['ContentType'])
421 1
            ? ['mimetype' => $meta['ContentType']] : false;
422
    }
423
424
    /**
425
     * @param string $path
426
     *
427
     * @return array|bool
428
     */
429 1
    public function getTimestamp($path)
430
    {
431 1
        $meta = $this->getMetadata($path);
432
433 1
        return isset($meta['LastModified'])
434 1
            ? ['timestamp' => strtotime($meta['LastModified'])] : false;
435
    }
436
437
    /**
438
     * @param string $path
439
     *
440
     * @return array|bool
441
     */
442 1
    public function getVisibility($path)
443
    {
444 1
        $meta = $this->client->getObjectAcl([
445 1
            'Bucket' => $this->getBucket(),
446 1
            'Key'    => $path,
447 1
        ]);
448
449 1
        foreach ($meta->get('Grants') as $grant) {
450 1
            if (isset($grant['Grantee']['URI'])
451 1
                && $grant['Permission'] === 'READ'
452 1
                && strpos($grant['Grantee']['URI'], 'global/AllUsers') !== false
453 1
            ) {
454
                return ['visibility' => AdapterInterface::VISIBILITY_PUBLIC];
455
            }
456 1
        }
457
458 1
        return ['visibility' => AdapterInterface::VISIBILITY_PRIVATE];
459
    }
460
461
    /**
462
     * @param array $content
463
     *
464
     * @return array
465
     */
466 1
    private function normalizeFileInfo(array $content)
467
    {
468 1
        $path = pathinfo($content['Key']);
469
470
        return [
471 1
            'type'      => 'file',
472 1
            'path'      => $content['Key'],
473 1
            'timestamp' => Carbon::parse($content['LastModified'])->getTimestamp(),
474 1
            'size'      => (int) $content['Size'],
475 1
            'dirname'   => (string) $path['dirname'],
476 1
            'basename'  => (string) $path['basename'],
477 1
            'extension' => isset($path['extension']) ? $path['extension'] : '',
478 1
            'filename'  => (string) $path['filename'],
479 1
        ];
480
    }
481
482
    /**
483
     * @param string $directory
484
     * @param bool   $recursive
485
     *
486
     * @return mixed
487
     */
488 2
    private function listObjects($directory = '', $recursive = false)
489
    {
490 2
        return $this->client->listObjects([
491 2
            'Bucket'    => $this->getBucket(),
492 2
            'Prefix'    => ((string) $directory === '') ? '' : ($directory.'/'),
493 2
            'Delimiter' => $recursive ? '' : '/',
494 2
        ]);
495
    }
496
}
497