Completed
Pull Request — master (#548)
by Albin
05:45
created

AwsS3   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 377
Duplicated Lines 17.24 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 6
dl 65
loc 377
rs 6.5957
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 18 1
A setMetadata() 0 10 2
A getMetadata() 0 4 2
B read() 0 24 6
B rename() 0 24 4
B write() 0 29 5
A exists() 0 10 2
A mtime() 14 14 4
A size() 14 14 4
A keys() 0 4 1
A listKeys() 0 22 3
A delete() 14 14 4
A isDirectory() 0 15 4
B ensureBucketExists() 0 25 4
A getOptions() 0 14 1
A computePath() 0 8 2
A computeKey() 0 4 1
A guessContentType() 10 10 2
A mimeType() 13 13 4

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like AwsS3 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 AwsS3, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Aws\S3\Exception\S3Exception;
6
use Gaufrette\Adapter;
7
use Aws\S3\S3Client;
8
use Gaufrette\Exception\FileNotFound;
9
use Gaufrette\Exception\StorageFailure;
10
use Gaufrette\Util;
11
12
/**
13
 * Amazon S3 adapter using the AWS SDK for PHP v2.x.
14
 *
15
 * @author  Michael Dowling <[email protected]>
16
 */
17
class AwsS3 implements Adapter,
0 ignored issues
show
Coding Style introduced by
The first item in a multi-line implements list must be on the line following the implements keyword
Loading history...
18
                       MetadataSupporter,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
19
                       ListKeysAware,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
20
                       SizeCalculator,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
21
                       MimeTypeProvider
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
22
{
23
    /** @var S3Client */
24
    protected $service;
25
    /** @var string */
26
    protected $bucket;
27
    /** @var array */
28
    protected $options;
29
    /** @var bool */
30
    protected $bucketExists;
31
    /** @var array */
32
    protected $metadata = [];
33
    /** @var bool */
34
    protected $detectContentType;
35
36
    /**
37
     * @param S3Client $service
38
     * @param string   $bucket
39
     * @param array    $options
40
     * @param bool     $detectContentType
41
     */
42
    public function __construct(S3Client $service, $bucket, array $options = [], $detectContentType = false)
43
    {
44
        $this->service = $service;
45
        $this->bucket = $bucket;
46
        $this->options = array_replace(
47
            [
48
                'create' => false,
49
                'directory' => '',
50
                'acl' => 'private',
51
            ],
52
            $options
53
        );
54
55
        // Remove trailing slash so it can't be doubled in computePath() method
56
        $this->options['directory'] = rtrim($this->options['directory'], '/');
57
58
        $this->detectContentType = $detectContentType;
59
    }
60
61
    /**
62
     * {@inheritdoc}
63
     */
64
    public function setMetadata($key, $metadata)
65
    {
66
        // BC with AmazonS3 adapter
67
        if (isset($metadata['contentType'])) {
68
            $metadata['ContentType'] = $metadata['contentType'];
69
            unset($metadata['contentType']);
70
        }
71
72
        $this->metadata[$key] = $metadata;
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function getMetadata($key)
79
    {
80
        return isset($this->metadata[$key]) ? $this->metadata[$key] : [];
81
    }
82
83
    /**
84
     * {@inheritdoc}
85
     */
86
    public function read($key)
87
    {
88
        $this->ensureBucketExists();
89
        $options = $this->getOptions($key);
90
91
        try {
92
            // Get remote object
93
            $object = $this->service->getObject($options);
94
            // If there's no metadata array set up for this object, set it up
95
            if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) {
96
                $this->metadata[$key] = [];
97
            }
98
            // Make remote ContentType metadata available locally
99
            $this->metadata[$key]['ContentType'] = $object->get('ContentType');
100
101
            return (string) $object->get('Body');
102
        } catch (\Exception $e) {
103
            if ($e instanceof S3Exception && $e->getResponse()->getStatusCode() === 404) {
104
                throw new FileNotFound($key);
105
            }
106
107
            throw StorageFailure::unexpectedFailure('read', ['key' => $key], $e);
108
        }
109
    }
110
111
    /**
112
     * {@inheritdoc}
113
     */
114
    public function rename($sourceKey, $targetKey)
115
    {
116
        $this->ensureBucketExists();
117
        $options = $this->getOptions(
118
            $targetKey,
119
            ['CopySource' => $this->bucket.'/'.$this->computePath($sourceKey)]
120
        );
121
122
        try {
123
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
124
125
            return $this->delete($sourceKey);
126
        } catch (\Exception $e) {
127
            if ($e instanceof S3Exception && $e->getResponse()->getStatusCode() === 404) {
128
                throw new FileNotFound($sourceKey);
129
            }
130
131
            throw StorageFailure::unexpectedFailure(
132
                'rename',
133
                ['sourceKey' => $sourceKey, 'targetKey' => $targetKey],
134
                $e
135
            );
136
        }
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142
    public function write($key, $content)
143
    {
144
        $this->ensureBucketExists();
145
        $options = $this->getOptions($key, ['Body' => $content]);
146
147
        /*
148
         * If the ContentType was not already set in the metadata, then we autodetect
149
         * it to prevent everything being served up as binary/octet-stream.
150
         */
151
        if (!isset($options['ContentType']) && $this->detectContentType) {
152
            $options['ContentType'] = $this->guessContentType($content);
153
        }
154
155
        try {
156
            $this->service->putObject($options);
157
158
            if (is_resource($content)) {
159
                return Util\Size::fromResource($content);
160
            }
161
162
            return Util\Size::fromContent($content);
163
        } catch (\Exception $e) {
164
            throw StorageFailure::unexpectedFailure(
165
                'write',
166
                ['key' => $key, 'content' => $content],
167
                $e
168
            );
169
        }
170
    }
171
172
    /**
173
     * {@inheritdoc}
174
     */
175
    public function exists($key)
176
    {
177
        $path = $this->computePath($key);
178
179
        try {
180
            return $this->service->doesObjectExist($this->bucket, $path);
181
        } catch (\Exception $exception) {
182
            throw StorageFailure::unexpectedFailure('exists', ['key' => $key], $exception);
183
        }
184
    }
185
186
    /**
187
     * {@inheritdoc}
188
     */
189 View Code Duplication
    public function mtime($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
190
    {
191
        try {
192
            $result = $this->service->headObject($this->getOptions($key));
193
194
            return strtotime($result['LastModified']);
195
        } catch (\Exception $e) {
196
            if ($e instanceof S3Exception && $e->getResponse()->getStatusCode() === 404) {
197
                throw new FileNotFound($key);
198
            }
199
200
            throw StorageFailure::unexpectedFailure('mtime', ['key' => $key], $e);
201
        }
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 View Code Duplication
    public function size($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
208
    {
209
        try {
210
            $result = $this->service->headObject($this->getOptions($key));
211
212
            return $result['ContentLength'];
213
        } catch (\Exception $e) {
214
            if ($e instanceof S3Exception && $e->getResponse()->getStatusCode() === 404) {
215
                throw new FileNotFound($key);
216
            }
217
218
            throw StorageFailure::unexpectedFailure('size', ['key' => $key], $e);
219
        }
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225
    public function keys()
226
    {
227
        return $this->listKeys();
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233
    public function listKeys($prefix = '')
234
    {
235
        $this->ensureBucketExists();
236
237
        $options = [
238
            'Bucket' => $this->bucket,
239
            'Prefix' => $this->computePath($prefix),
240
        ];
241
242
        $keys = [];
243
        $objects = $this->service->getIterator('ListObjects', $options);
244
245
        try {
246
            foreach ($objects as $file) {
247
                $keys[] = $this->computeKey($file['Key']);
248
            }
249
        } catch (S3Exception $e) {
250
            throw StorageFailure::unexpectedFailure('listKeys', ['prefix' => $prefix], $e);
251
        }
252
253
        return $keys;
254
    }
255
256
    /**
257
     * {@inheritdoc}
258
     */
259 View Code Duplication
    public function delete($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260
    {
261
        try {
262
            $this->service->deleteObject($this->getOptions($key));
263
264
            return true;
265
        } catch (\Exception $e) {
266
            if ($e instanceof S3Exception && $e->getResponse()->getStatusCode() === 404) {
267
                throw new FileNotFound($key);
268
            }
269
270
            throw StorageFailure::unexpectedFailure('delete', ['key' => $key], $e);
271
        }
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277
    public function isDirectory($key)
278
    {
279
        $result = $this->service->listObjects([
280
            'Bucket' => $this->bucket,
281
            'Prefix' => rtrim($this->computePath($key), '/').'/',
282
            'MaxKeys' => 1,
283
        ]);
284
        if (isset($result['Contents'])) {
285
            if (is_array($result['Contents']) || $result['Contents'] instanceof \Countable) {
286
                return count($result['Contents']) > 0;
287
            }
288
        }
289
290
        return false;
291
    }
292
293
    /**
294
     * Ensures the specified bucket exists. If the bucket does not exists
295
     * and the create option is set to true, it will try to create the
296
     * bucket. The bucket is created using the same region as the supplied
297
     * client object.
298
     *
299
     * @throws \RuntimeException if the bucket does not exists or could not be
300
     *                           created
301
     */
302
    protected function ensureBucketExists()
303
    {
304
        if ($this->bucketExists) {
305
            return true;
306
        }
307
308
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
309
            return true;
310
        }
311
312
        if (!$this->options['create']) {
313
            throw new \RuntimeException(sprintf(
314
                'The configured bucket "%s" does not exist.',
315
                $this->bucket
316
            ));
317
        }
318
319
        $this->service->createBucket([
320
            'Bucket' => $this->bucket,
321
            'LocationConstraint' => $this->service->getRegion()
322
        ]);
323
        $this->bucketExists = true;
324
325
        return true;
326
    }
327
328
    protected function getOptions($key, array $options = [])
329
    {
330
        $options['ACL'] = $this->options['acl'];
331
        $options['Bucket'] = $this->bucket;
332
        $options['Key'] = $this->computePath($key);
333
334
        /*
335
         * Merge global options for adapter, which are set in the constructor, with metadata.
336
         * Metadata will override global options.
337
         */
338
        $options = array_merge($this->options, $options, $this->getMetadata($key));
339
340
        return $options;
341
    }
342
343
    protected function computePath($key)
344
    {
345
        if (empty($this->options['directory'])) {
346
            return $key;
347
        }
348
349
        return sprintf('%s/%s', $this->options['directory'], $key);
350
    }
351
352
    /**
353
     * Computes the key from the specified path.
354
     *
355
     * @param string $path
356
     *
357
     * return string
358
     */
359
    protected function computeKey($path)
360
    {
361
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
362
    }
363
364
    /**
365
     * @param string $content
366
     *
367
     * @return string
368
     */
369 View Code Duplication
    private function guessContentType($content)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370
    {
371
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
372
373
        if (is_resource($content)) {
374
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
375
        }
376
377
        return $fileInfo->buffer($content);
378
    }
379
380 View Code Duplication
    public function mimeType($key)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
381
    {
382
        try {
383
            $result = $this->service->headObject($this->getOptions($key));
384
            return ($result['ContentType']);
385
        } catch (\Exception $e) {
386
            if ($e instanceof S3Exception && $e->getResponse()->getStatusCode() === 404) {
387
                throw new FileNotFound($key);
388
            }
389
390
            throw StorageFailure::unexpectedFailure('mimeType', ['key' => $key], $e);
391
        }
392
    }
393
}
394