GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

CosAdapter   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

Changes 15
Bugs 1 Features 0
Metric Value
wmc 56
eloc 134
c 15
b 1
f 0
dl 0
loc 369
rs 5.5199

28 Methods

Rating   Name   Duplication   Size   Complexity  
A readStream() 0 11 2
A visibility() 0 13 4
A listObjects() 0 19 6
A writeStream() 0 3 1
A read() 0 10 2
A write() 0 11 3
A normalizeVisibility() 0 3 2
A createDirectory() 0 5 1
A move() 0 5 1
A setBucketClient() 0 5 1
A getBucketClient() 0 3 1
A fileExists() 0 3 1
A getUrl() 0 9 3
A fileSize() 0 8 2
A getSignedUrl() 0 5 1
A mimeType() 0 7 2
A toResource() 0 9 2
A delete() 0 8 2
A __construct() 0 13 1
A copy() 0 16 2
A getObjectClient() 0 3 1
A getSourcePath() 0 8 1
A setObjectClient() 0 5 1
A listContents() 0 17 3
A lastModified() 0 8 2
A setVisibility() 0 7 1
A deleteDirectory() 0 28 3
A getMetadata() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like CosAdapter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CosAdapter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Overtrue\Flysystem\Cos;
4
5
use GuzzleHttp\Psr7\Uri;
6
use League\Flysystem\Config;
7
use League\Flysystem\DirectoryAttributes;
8
use League\Flysystem\FileAttributes;
9
use League\Flysystem\FilesystemAdapter;
10
use League\Flysystem\PathPrefixer;
11
use League\Flysystem\UnableToCopyFile;
12
use League\Flysystem\UnableToDeleteDirectory;
13
use League\Flysystem\UnableToDeleteFile;
14
use League\Flysystem\UnableToReadFile;
15
use League\Flysystem\UnableToRetrieveMetadata;
16
use League\Flysystem\UnableToWriteFile;
17
use League\Flysystem\Visibility;
18
use Overtrue\CosClient\ObjectClient;
19
use Overtrue\CosClient\BucketClient;
20
21
class CosAdapter implements FilesystemAdapter
22
{
23
    /**
24
     * @var \Overtrue\CosClient\ObjectClient|null
25
     */
26
    protected ?ObjectClient $objectClient;
27
28
    /**
29
     * @var \Overtrue\CosClient\BucketClient|null
30
     */
31
    protected ?BucketClient $bucketClient;
32
33
    /**
34
     * @var PathPrefixer
35
     */
36
    protected PathPrefixer $prefixer;
37
38
    /**
39
     * @var array
40
     */
41
    protected $config;
42
43
    /**
44
     * CosAdapter constructor.
45
     *
46
     * @param array $config
47
     */
48
    public function __construct(array $config)
49
    {
50
        $this->config = \array_merge(
51
            [
52
                'bucket' => null,
53
                'app_id' => null,
54
                'region' => 'ap-guangzhou',
55
                'signed_url' => false,
56
            ],
57
            $config
58
        );
59
60
        $this->prefixer = new PathPrefixer($config['prefix'] ?? '', DIRECTORY_SEPARATOR);
61
    }
62
63
    public function fileExists(string $path): bool
64
    {
65
        return $this->getMetadata($path) !== null;
66
    }
67
68
    public function write(string $path, string $contents, Config $config): void
69
    {
70
        $prefixedPath = $this->prefixer->prefixPath($path);
71
        $response = $this->getObjectClient()->putObject($prefixedPath, $contents, $config->get('headers', []));
72
73
        if (!$response->isSuccessful()) {
74
            throw UnableToWriteFile::atLocation($path, (string)$response->getBody());
75
        }
76
77
        if ($visibility = $config->get('visibility')) {
78
            $this->setVisibility($path, $visibility);
79
        }
80
    }
81
82
    public function writeStream(string $path, $contents, Config $config): void
83
    {
84
        $this->write($path, \stream_get_contents($contents), $config);
85
    }
86
87
    public function readStream(string $path)
88
    {
89
        $prefixedPath = $this->prefixer->prefixPath($path);
90
91
        $response = $this->getObjectClient()->get(\urlencode($prefixedPath), ['stream' => true]);
92
93
        if ($response->isNotFound()) {
94
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the return type mandated by League\Flysystem\FilesystemAdapter::readStream() of resource.

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...
95
        }
96
97
        return $response->getBody()->detach();
98
    }
99
100
    public function read(string $path): string
101
    {
102
        $prefixedPath = $this->prefixer->prefixPath($path);
103
104
        $response = $this->getObjectClient()->getObject($prefixedPath);
105
        if ($response->isNotFound()) {
106
            throw UnableToReadFile::fromLocation($path, (string)$response->getBody());
107
        }
108
109
        return (string)$response->getBody();
110
    }
111
112
    public function move(string $source, string $destination, Config $config): void
113
    {
114
        $this->copy($source, $destination, $config);
115
116
        $this->delete($this->prefixer->prefixPath($source));
117
    }
118
119
    public function copy(string $source, string $destination, Config $config): void
120
    {
121
        $prefixedSource = $this->prefixer->prefixPath($source);
122
123
        $location = $this->getSourcePath($prefixedSource);
124
125
        $prefixedDestination = $this->prefixer->prefixPath($destination);
126
127
        $response = $this->getObjectClient()->copyObject(
128
            $prefixedDestination,
129
            [
130
                'x-cos-copy-source' => $location,
131
            ]
132
        );
133
        if (!$response->isSuccessful()) {
134
            throw UnableToCopyFile::fromLocationTo($source, $destination);
135
        }
136
    }
137
138
    public function delete(string $path): void
139
    {
140
        $prefixedPath = $this->prefixer->prefixPath($path);
141
142
        $response = $this->getObjectClient()->deleteObject($prefixedPath);
143
144
        if (!$response->isSuccessful()) {
145
            throw UnableToDeleteFile::atLocation($path, (string)$response->getBody());
146
        }
147
    }
148
149
    public function listContents(string $path, bool $deep): iterable
150
    {
151
        $prefixedPath = $this->prefixer->prefixPath($path);
152
153
        $response = $this->listObjects($prefixedPath, $deep);
154
155
        // 处理目录
156
        foreach ($response['CommonPrefixes'] ?? [] as $prefix) {
157
            yield new DirectoryAttributes($prefix);
158
        }
159
160
        foreach ($response['Contents'] ?? [] as $content) {
161
            yield new FileAttributes(
162
                $content['Key'],
163
                \intval($content['Size']),
164
                null,
165
                \strtotime($content['LastModified'])
166
            );
167
        }
168
    }
169
170
    public function getMetadata($path): ?FileAttributes
171
    {
172
        $prefixedPath = $this->prefixer->prefixPath($path);
173
174
        $meta = $this->getObjectClient()->headObject($prefixedPath)->getHeaders();
175
        if (empty($meta)) {
176
            return null;
177
        }
178
179
        return new FileAttributes(
180
            $path,
181
            isset($meta['Content-Length'][0]) ? \intval($meta['Content-Length'][0]) : null,
182
            null,
183
            isset($meta['Last-Modified'][0]) ? \strtotime($meta['Last-Modified'][0]) : null,
184
            $meta['Content-Type'][0] ?? null,
185
        );
186
    }
187
188
    public function fileSize(string $path): FileAttributes
189
    {
190
        $meta = $this->getMetadata($path);
191
        if ($meta->fileSize() === null) {
192
            throw UnableToRetrieveMetadata::fileSize($path);
193
        }
194
195
        return $meta;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $meta could return the type null which is incompatible with the type-hinted return League\Flysystem\FileAttributes. Consider adding an additional type-check to rule them out.
Loading history...
196
    }
197
198
    public function mimeType(string $path): FileAttributes
199
    {
200
        $meta = $this->getMetadata($path);
201
        if ($meta->mimeType() === null) {
202
            throw UnableToRetrieveMetadata::mimeType($path);
203
        }
204
        return $meta;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $meta could return the type null which is incompatible with the type-hinted return League\Flysystem\FileAttributes. Consider adding an additional type-check to rule them out.
Loading history...
205
    }
206
207
    public function lastModified(string $path): FileAttributes
208
    {
209
        $meta = $this->getMetadata($path);
210
        if ($meta->lastModified() === null) {
211
            throw UnableToRetrieveMetadata::lastModified($path);
212
        }
213
214
        return $meta;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $meta could return the type null which is incompatible with the type-hinted return League\Flysystem\FileAttributes. Consider adding an additional type-check to rule them out.
Loading history...
215
    }
216
217
    public function visibility(string $path): FileAttributes
218
    {
219
        $prefixedPath = $this->prefixer->prefixPath($path);
220
221
        $meta = $this->getObjectClient()->getObjectACL($prefixedPath);
222
223
        foreach ($meta['AccessControlPolicy']['AccessControlList']['Grant'] ?? [] as $grant) {
224
            if ('READ' === $grant['Permission'] && false !== strpos($grant['Grantee']['URI'] ?? '', 'global/AllUsers')) {
225
                return new FileAttributes($path, null, Visibility::PUBLIC);
226
            }
227
        }
228
229
        return new FileAttributes($path, null, Visibility::PRIVATE);
230
    }
231
232
    public function setVisibility(string $path, string $visibility): void
233
    {
234
        $this->getObjectClient()->putObjectACL(
235
            $this->prefixer->prefixPath($path),
236
            [],
237
            [
238
                'x-cos-acl' => $this->normalizeVisibility($visibility),
239
            ]
240
        );
241
    }
242
243
    public function createDirectory(string $path, Config $config): void
244
    {
245
        $dirname = $this->prefixer->prefixPath($path);
246
247
        $this->getObjectClient()->putObject($dirname . '/', '');
248
    }
249
250
    public function deleteDirectory(string $path): void
251
    {
252
        $dirname = $this->prefixer->prefixPath($path);
253
254
        $response = $this->listObjects($dirname);
255
256
        if (empty($response['Contents'])) {
257
            return;
258
        }
259
260
        $keys = array_map(
261
            function ($item) {
262
                return ['Key' => $item['Key']];
263
            },
264
            $response['Contents']
265
        );
266
267
        $response = $this->getObjectClient()->deleteObjects(
268
            [
269
                'Delete' => [
270
                    'Quiet' => 'false',
271
                    'Object' => $keys,
272
                ],
273
            ]
274
        );
275
276
        if (!$response->isSuccessful()) {
277
            throw UnableToDeleteDirectory::atLocation($path, (string) $response->getBody());
278
        }
279
    }
280
281
    public function getUrl(string $path)
282
    {
283
        $prefixedPath = $this->prefixer->prefixPath($path);
284
285
        if (!empty($this->config['cdn'])) {
286
            return \strval(new Uri(\sprintf('%s/%s', \rtrim($this->config['cdn'], '/'), $prefixedPath)));
287
        }
288
289
        return $this->config['signed_url'] ? $this->getSignedUrl($path) : $this->getObjectClient()->getObjectUrl($prefixedPath);
290
    }
291
292
    public function getSignedUrl(string $path, string $expires = '+60 minutes'): string
293
    {
294
        $prefixedPath = $this->prefixer->prefixPath($path);
295
296
        return $this->getObjectClient()->getObjectSignedUrl($prefixedPath, $expires);
297
    }
298
299
    public function getObjectClient()
300
    {
301
        return $this->objectClient ?? $this->objectClient = new ObjectClient($this->config);
302
    }
303
304
    public function getBucketClient()
305
    {
306
        return $this->bucketClient ?? $this->bucketClient = new BucketClient($this->config);
307
    }
308
309
    /**
310
     * @param \Overtrue\CosClient\ObjectClient $objectClient
311
     *
312
     * @return $this
313
     */
314
    public function setObjectClient(ObjectClient $objectClient)
315
    {
316
        $this->objectClient = $objectClient;
317
318
        return $this;
319
    }
320
321
    /**
322
     * @param \Overtrue\CosClient\BucketClient $bucketClient
323
     *
324
     * @return $this
325
     */
326
    public function setBucketClient(BucketClient $bucketClient)
327
    {
328
        $this->bucketClient = $bucketClient;
329
330
        return $this;
331
    }
332
333
    /**
334
     * @param string $path
335
     *
336
     * @return string
337
     */
338
    protected function getSourcePath(string $path)
339
    {
340
        return sprintf(
341
            '%s-%s.cos.%s.myqcloud.com/%s',
342
            $this->config['bucket'],
343
            $this->config['app_id'],
344
            $this->config['region'],
345
            $path
346
        );
347
    }
348
349
    /**
350
     * @param string $directory
351
     * @param bool $recursive
352
     *
353
     * @return mixed
354
     */
355
    protected function listObjects($directory = '', $recursive = false)
356
    {
357
        $result = $this->getBucketClient()->getObjects(
358
            [
359
                'prefix' => ('' === (string)$directory) ? '' : ($directory . '/'),
360
                'delimiter' => $recursive ? '' : '/',
361
            ]
362
        )['ListBucketResult'];
363
364
        foreach (['CommonPrefixes', 'Contents'] as $key) {
365
            $result[$key] = $result[$key] ?? [];
366
367
            // 确保是二维数组
368
            if (($index = \key($result[$key])) !== 0) {
369
                $result[$key] = \is_null($index) ? [] : [$result[$key]];
370
            }
371
        }
372
373
        return $result;
374
    }
375
376
    protected function normalizeVisibility(string $visibility): string
377
    {
378
        return $visibility === Visibility::PUBLIC ? 'public-read' : 'default';
379
    }
380
381
    protected function toResource(string $body)
382
    {
383
        $resource = fopen('php://temp', 'r+');
384
        if ($body !== '') {
385
            fwrite($resource, $body);
386
            fseek($resource, 0);
387
        }
388
389
        return $resource;
390
    }
391
}
392