Completed
Pull Request — master (#414)
by Albin
02:59
created

AwsS3::guessContentType()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 10
Ratio 100 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 10
loc 10
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
3
namespace Gaufrette\Adapter;
4
5
use Gaufrette\Adapter;
6
use Aws\S3\S3Client;
7
use Gaufrette\Util;
8
9
/**
10
 * Amazon S3 adapter using the AWS SDK for PHP v2.x.
11
 *
12
 * @author  Michael Dowling <[email protected]>
13
 */
14
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...
15
                       MetadataSupporter,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
16
                       ListKeysAware,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
17
                       SizeCalculator
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
18
{
19
    protected $service;
20
    protected $bucket;
21
    protected $options;
22
    protected $bucketExists;
23
    protected $metadata = array();
24
    protected $detectContentType;
25
26 View Code Duplication
    public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = false)
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...
27
    {
28
        $this->service = $service;
29
        $this->bucket = $bucket;
30
        $this->options = array_replace(
31
            array(
32
                'create' => false,
33
                'directory' => '',
34
                'acl' => 'private',
35
            ),
36
            $options
37
        );
38
39
        $this->detectContentType = $detectContentType;
40
    }
41
42
    /**
43
     * Gets the publicly accessible URL of an Amazon S3 object.
44
     *
45
     * @param string $key     Object key
46
     * @param array  $options Associative array of options used to buld the URL
47
     *                        - expires: The time at which the URL should expire
48
     *                        represented as a UNIX timestamp
49
     *                        - Any options available in the Amazon S3 GetObject
50
     *                        operation may be specified.
51
     *
52
     * @return string
53
     */
54
    public function getUrl($key, array $options = array())
55
    {
56
        return $this->service->getObjectUrl(
57
            $this->bucket,
58
            $this->computePath($key),
59
            isset($options['expires']) ? $options['expires'] : null,
60
            $options
61
        );
62
    }
63
64
    /**
65
     * {@inheritdoc}
66
     */
67
    public function setMetadata($key, $metadata)
68
    {
69
        // BC with AmazonS3 adapter
70
        if (isset($metadata['contentType'])) {
71
            $metadata['ContentType'] = $metadata['contentType'];
72
            unset($metadata['contentType']);
73
        }
74
75
        $this->metadata[$key] = $metadata;
76
    }
77
78
    /**
79
     * {@inheritdoc}
80
     */
81
    public function getMetadata($key)
82
    {
83
        return isset($this->metadata[$key]) ? $this->metadata[$key] : array();
84
    }
85
86
    /**
87
     * {@inheritdoc}
88
     */
89
    public function read($key)
90
    {
91
        $this->ensureBucketExists();
92
        $options = $this->getOptions($key);
93
94
        try {
95
            return (string) $this->service->getObject($options)->get('Body');
96
        } catch (\Exception $e) {
97
            return false;
98
        }
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function rename($sourceKey, $targetKey)
105
    {
106
        $this->ensureBucketExists();
107
        $options = $this->getOptions(
108
            $targetKey,
109
            array(
110
                'CopySource' => $this->bucket.'/'.$this->computePath($sourceKey),
111
            )
112
        );
113
114
        try {
115
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
116
117
            return $this->delete($sourceKey);
118
        } catch (\Exception $e) {
119
            return false;
120
        }
121
    }
122
123
    /**
124
     * {@inheritdoc}
125
     */
126
    public function write($key, $content)
127
    {
128
        $this->ensureBucketExists();
129
        $options = $this->getOptions($key, array('Body' => $content));
130
131
        /*
132
         * If the ContentType was not already set in the metadata, then we autodetect
133
         * it to prevent everything being served up as binary/octet-stream.
134
         */
135
        if (!isset($options['ContentType']) && $this->detectContentType) {
136
            $options['ContentType'] = $this->guessContentType($content);
137
        }
138
139
        try {
140
            $this->service->putObject($options);
141
142
            if (is_resource($content)) {
143
                return Util\Size::fromResource($content);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Gaufrette\Util\S...fromResource($content); (string) is incompatible with the return type declared by the interface Gaufrette\Adapter::write of type integer|boolean.

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...
144
            }
145
146
            return Util\Size::fromContent($content);
147
        } catch (\Exception $e) {
148
            return false;
149
        }
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155
    public function exists($key)
156
    {
157
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
158
    }
159
160
    /**
161
     * {@inheritdoc}
162
     */
163 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...
164
    {
165
        try {
166
            $result = $this->service->headObject($this->getOptions($key));
167
168
            return strtotime($result['LastModified']);
169
        } catch (\Exception $e) {
170
            return false;
171
        }
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 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...
178
    {
179
        try {
180
            $result = $this->service->headObject($this->getOptions($key));
181
182
            return $result['ContentLength'];
183
        } catch (\Exception $e) {
184
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Gaufrette\Adapter\SizeCalculator::size of type integer.

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...
185
        }
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function keys()
192
    {
193
        return $this->listKeys();
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function listKeys($prefix = '')
200
    {
201
        $options = array('Bucket' => $this->bucket);
202 View Code Duplication
        if ((string) $prefix != '') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
203
            $options['Prefix'] = $this->computePath($prefix);
204
        } elseif (!empty($this->options['directory'])) {
205
            $options['Prefix'] = $this->options['directory'];
206
        }
207
208
        $keys = array();
209
        $iter = $this->service->getIterator('ListObjects', $options);
210
        foreach ($iter as $file) {
211
            $keys[] = $this->computeKey($file['Key']);
212
        }
213
214
        return $keys;
215
    }
216
217
    /**
218
     * {@inheritdoc}
219
     */
220
    public function delete($key)
221
    {
222
        try {
223
            $this->service->deleteObject($this->getOptions($key));
224
225
            return true;
226
        } catch (\Exception $e) {
227
            return false;
228
        }
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234
    public function isDirectory($key)
235
    {
236
        $result = $this->service->listObjects(array(
237
            'Bucket' => $this->bucket,
238
            'Prefix' => rtrim($this->computePath($key), '/').'/',
239
            'MaxKeys' => 1,
240
        ));
241
242
        return count($result['Contents']) > 0;
243
    }
244
245
    /**
246
     * Ensures the specified bucket exists. If the bucket does not exists
247
     * and the create option is set to true, it will try to create the
248
     * bucket. The bucket is created using the same region as the supplied
249
     * client object.
250
     *
251
     * @throws \RuntimeException if the bucket does not exists or could not be
252
     *                           created
253
     */
254
    protected function ensureBucketExists()
255
    {
256
        if ($this->bucketExists) {
257
            return true;
258
        }
259
260
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
261
            return true;
262
        }
263
264
        if (!$this->options['create']) {
265
            throw new \RuntimeException(sprintf(
266
                'The configured bucket "%s" does not exist.',
267
                $this->bucket
268
            ));
269
        }
270
271
        $options = array('Bucket' => $this->bucket);
272
        if ($this->service->getRegion() != 'us-east-1') {
273
            $options['LocationConstraint'] = $this->service->getRegion();
274
        }
275
276
        $this->service->createBucket($options);
277
        $this->bucketExists = true;
278
279
        return true;
280
    }
281
282
    protected function getOptions($key, array $options = array())
283
    {
284
        $options['ACL'] = $this->options['acl'];
285
        $options['Bucket'] = $this->bucket;
286
        $options['Key'] = $this->computePath($key);
287
288
        /*
289
         * Merge global options for adapter, which are set in the constructor, with metadata.
290
         * Metadata will override global options.
291
         */
292
        $options = array_merge($this->options, $options, $this->getMetadata($key));
293
294
        return $options;
295
    }
296
297 View Code Duplication
    protected function computePath($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...
298
    {
299
        if (empty($this->options['directory'])) {
300
            return $key;
301
        }
302
303
        return sprintf('%s/%s', $this->options['directory'], $key);
304
    }
305
306
    /**
307
     * Computes the key from the specified path.
308
     *
309
     * @param string $path
310
     *
311
     * return string
312
     */
313
    protected function computeKey($path)
314
    {
315
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
316
    }
317
318
    /**
319
     * @param string $content
320
     *
321
     * @return string
322
     */
323 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...
324
    {
325
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
326
327
        if (is_resource($content)) {
328
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
329
        }
330
331
        return $fileInfo->buffer($content);
332
    }
333
}
334