Completed
Push — master ( d64c7f...d0ede0 )
by Nicolas
11s
created

src/Gaufrette/Adapter/AwsS3.php (12 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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,
15
                       MetadataSupporter,
16
                       ListKeysAware,
17
                       SizeCalculator,
18
                       MimeTypeProvider
19
{
20
    /** @var S3Client */
21
    protected $service;
22
    /** @var string */
23
    protected $bucket;
24
    /** @var array */
25
    protected $options;
26
    /** @var bool */
27
    protected $bucketExists;
28
    /** @var array */
29
    protected $metadata = [];
30
    /** @var bool */
31
    protected $detectContentType;
32
33
    /**
34
     * @param S3Client $service
35
     * @param string   $bucket
36
     * @param array    $options
37
     * @param bool     $detectContentType
38
     */
39 View Code Duplication
    public function __construct(S3Client $service, $bucket, array $options = [], $detectContentType = false)
0 ignored issues
show
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...
40
    {
41
        $this->service = $service;
42
        $this->bucket = $bucket;
43
        $this->options = array_replace(
44
            [
45
                'create' => false,
46
                'directory' => '',
47
                'acl' => 'private',
48
            ],
49
            $options
50
        );
51
52
        $this->detectContentType = $detectContentType;
53
    }
54
55
    /**
56
     * Gets the publicly accessible URL of an Amazon S3 object.
57
     *
58
     * @param string $key     Object key
59
     * @param array  $options Associative array of options used to buld the URL
60
     *                        - expires: The time at which the URL should expire
61
     *                        represented as a UNIX timestamp
62
     *                        - Any options available in the Amazon S3 GetObject
63
     *                        operation may be specified.
64
     *
65
     * @return string
66
     *
67
     * @deprecated 1.0 Resolving object path into URLs is out of the scope of this repository since v0.4. gaufrette/extras
68
     *                 provides a Filesystem decorator with a regular resolve() method. You should use it instead.
69
     *
70
     * @see https://github.com/Gaufrette/extras
71
     */
72
    public function getUrl($key, array $options = [])
73
    {
74
        @trigger_error(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
75
            E_USER_DEPRECATED,
76
            'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature'
77
        );
78
79
        return $this->service->getObjectUrl(
80
            $this->bucket,
81
            $this->computePath($key),
82
            isset($options['expires']) ? $options['expires'] : null,
0 ignored issues
show
The call to S3Client::getObjectUrl() has too many arguments starting with isset($options['expires'...tions['expires'] : null.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
83
            $options
84
        );
85
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90
    public function setMetadata($key, $metadata)
91
    {
92
        // BC with AmazonS3 adapter
93
        if (isset($metadata['contentType'])) {
94
            $metadata['ContentType'] = $metadata['contentType'];
95
            unset($metadata['contentType']);
96
        }
97
98
        $this->metadata[$key] = $metadata;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104
    public function getMetadata($key)
105
    {
106
        return isset($this->metadata[$key]) ? $this->metadata[$key] : [];
107
    }
108
109
    /**
110
     * {@inheritdoc}
111
     */
112
    public function read($key)
113
    {
114
        $this->ensureBucketExists();
115
        $options = $this->getOptions($key);
116
117
        try {
118
            // Get remote object
119
            $object = $this->service->getObject($options);
120
            // If there's no metadata array set up for this object, set it up
121
            if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) {
122
                $this->metadata[$key] = [];
123
            }
124
            // Make remote ContentType metadata available locally
125
            $this->metadata[$key]['ContentType'] = $object->get('ContentType');
126
127
            return (string) $object->get('Body');
128
        } catch (\Exception $e) {
129
            return false;
130
        }
131
    }
132
133
    /**
134
     * {@inheritdoc}
135
     */
136
    public function rename($sourceKey, $targetKey)
137
    {
138
        $this->ensureBucketExists();
139
        $options = $this->getOptions(
140
            $targetKey,
141
            ['CopySource' => $this->bucket.'/'.$this->computePath($sourceKey)]
142
        );
143
144
        try {
145
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
146
147
            return $this->delete($sourceKey);
148
        } catch (\Exception $e) {
149
            return false;
150
        }
151
    }
152
153
    /**
154
     * {@inheritdoc}
155
     */
156
    public function write($key, $content)
157
    {
158
        $this->ensureBucketExists();
159
        $options = $this->getOptions($key, ['Body' => $content]);
160
161
        /*
162
         * If the ContentType was not already set in the metadata, then we autodetect
163
         * it to prevent everything being served up as binary/octet-stream.
164
         */
165
        if (!isset($options['ContentType']) && $this->detectContentType) {
166
            $options['ContentType'] = $this->guessContentType($content);
167
        }
168
169
        try {
170
            $this->service->putObject($options);
171
172
            if (is_resource($content)) {
173
                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...
174
            }
175
176
            return Util\Size::fromContent($content);
177
        } catch (\Exception $e) {
178
            return false;
179
        }
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function exists($key)
186
    {
187
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 View Code Duplication
    public function mtime($key)
0 ignored issues
show
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...
194
    {
195
        try {
196
            $result = $this->service->headObject($this->getOptions($key));
197
198
            return strtotime($result['LastModified']);
199
        } catch (\Exception $e) {
200
            return false;
201
        }
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207 View Code Duplication
    public function size($key)
0 ignored issues
show
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
            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...
215
        }
216
    }
217
218
    /**
219
     * {@inheritdoc}
220
     */
221
    public function keys()
222
    {
223
        return $this->listKeys();
224
    }
225
226
    /**
227
     * {@inheritdoc}
228
     */
229
    public function listKeys($prefix = '')
230
    {
231
        $this->ensureBucketExists();
232
233
        $options = ['Bucket' => $this->bucket];
234 View Code Duplication
        if ((string) $prefix != '') {
0 ignored issues
show
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...
235
            $options['Prefix'] = $this->computePath($prefix);
236
        } elseif (!empty($this->options['directory'])) {
237
            $options['Prefix'] = $this->options['directory'];
238
        }
239
240
        $keys = [];
241
        $iter = $this->service->getIterator('ListObjects', $options);
242
        foreach ($iter as $file) {
243
            $keys[] = $this->computeKey($file['Key']);
244
        }
245
246
        return $keys;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252
    public function delete($key)
253
    {
254
        try {
255
            $this->service->deleteObject($this->getOptions($key));
256
257
            return true;
258
        } catch (\Exception $e) {
259
            return false;
260
        }
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266
    public function isDirectory($key)
267
    {
268
        $result = $this->service->listObjects([
269
            'Bucket' => $this->bucket,
270
            'Prefix' => rtrim($this->computePath($key), '/').'/',
271
            'MaxKeys' => 1,
272
        ]);
273
        if (isset($result['Contents'])) {
274
            if (is_array($result['Contents']) || $result['Contents'] instanceof \Countable) {
275
                return count($result['Contents']) > 0;
276
            }
277
        }
278
279
        return false;
280
    }
281
282
    /**
283
     * Ensures the specified bucket exists. If the bucket does not exists
284
     * and the create option is set to true, it will try to create the
285
     * bucket. The bucket is created using the same region as the supplied
286
     * client object.
287
     *
288
     * @throws \RuntimeException if the bucket does not exists or could not be
289
     *                           created
290
     */
291
    protected function ensureBucketExists()
292
    {
293
        if ($this->bucketExists) {
294
            return true;
295
        }
296
297
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
298
            return true;
299
        }
300
301
        if (!$this->options['create']) {
302
            throw new \RuntimeException(sprintf(
303
                'The configured bucket "%s" does not exist.',
304
                $this->bucket
305
            ));
306
        }
307
308
        $this->service->createBucket([
309
            'Bucket' => $this->bucket,
310
            'LocationConstraint' => $this->service->getRegion()
311
        ]);
312
        $this->bucketExists = true;
313
314
        return true;
315
    }
316
317
    protected function getOptions($key, array $options = [])
318
    {
319
        $options['ACL'] = $this->options['acl'];
320
        $options['Bucket'] = $this->bucket;
321
        $options['Key'] = $this->computePath($key);
322
323
        /*
324
         * Merge global options for adapter, which are set in the constructor, with metadata.
325
         * Metadata will override global options.
326
         */
327
        $options = array_merge($this->options, $options, $this->getMetadata($key));
328
329
        return $options;
330
    }
331
332 View Code Duplication
    protected function computePath($key)
0 ignored issues
show
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...
333
    {
334
        if (empty($this->options['directory'])) {
335
            return $key;
336
        }
337
338
        return sprintf('%s/%s', $this->options['directory'], $key);
339
    }
340
341
    /**
342
     * Computes the key from the specified path.
343
     *
344
     * @param string $path
345
     *
346
     * return string
347
     */
348
    protected function computeKey($path)
349
    {
350
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
351
    }
352
353
    /**
354
     * @param string $content
355
     *
356
     * @return string
357
     */
358 View Code Duplication
    private function guessContentType($content)
0 ignored issues
show
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...
359
    {
360
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
361
362
        if (is_resource($content)) {
363
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
364
        }
365
366
        return $fileInfo->buffer($content);
367
    }
368
369 View Code Duplication
    public function mimeType($key)
0 ignored issues
show
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
        try {
372
            $result = $this->service->headObject($this->getOptions($key));
373
            return ($result['ContentType']);
374
        } catch (\Exception $e) {
375
            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\MimeTypeProvider::mimeType of type string.

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...
376
        }
377
    }
378
}
379