Completed
Pull Request — master (#421)
by Vincent
03:27
created

AwsS3::listFiles()   C

Complexity

Conditions 7
Paths 27

Size

Total Lines 28
Code Lines 19

Duplication

Lines 5
Ratio 17.86 %

Importance

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
152
            }
153
        } catch (\Exception $e) {
154
            return false;
155
        }
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161
    public function exists($key)
162
    {
163
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169 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...
170
    {
171
        try {
172
            $result = $this->service->headObject($this->getOptions($key));
173
174
            return strtotime($result['LastModified']);
175
        } catch (\Exception $e) {
176
            return false;
177
        }
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 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...
184
    {
185
        try {
186
            $result = $this->service->headObject($this->getOptions($key));
187
188
            return $result['ContentLength'];
189
        } catch (\Exception $e) {
190
            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...
191
        }
192
    }
193
194
    /**
195
     * {@inheritdoc}
196
     */
197
    public function keys()
198
    {
199
        return $this->listKeys();
200
    }
201
202
    /**
203
     * {@inheritdoc}
204
     */
205
    public function listKeys($prefix = '')
206
    {
207
        $options = array('Bucket' => $this->bucket);
208 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...
209
            $options['Prefix'] = $this->computePath($prefix);
210
        } elseif (!empty($this->options['directory'])) {
211
            $options['Prefix'] = $this->options['directory'];
212
        }
213
214
        $keys = array();
215
        $iter = $this->service->getIterator('ListObjects', $options);
216
        foreach ($iter as $file) {
217
            $keys[] = $this->computeKey($file['Key']);
218
        }
219
220
        return $keys;
221
    }
222
223
    /**
224
     * List files beginning with given prefix.
225
     * Similar to listKeys but this will also return file mtime, size, and etag.
226
     * @param string $prefix
227
     * @return mixed
228
     */
229
    public function listFiles($prefix = '')
230
    {
231
        $options = array('Bucket' => $this->bucket);
232 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...
233
            $options['Prefix'] = $this->computePath($prefix);
234
        } elseif (!empty($this->options['directory'])) {
235
            $options['Prefix'] = $this->options['directory'];
236
        }
237
238
        $files = array();
239
        $iter = $this->service->getIterator('ListObjects', $options);
240
        foreach ($iter as $file) {
241
            $item= [];
242
            $item["key"] = $this->computeKey($file['Key']);
243
            if (isset($file["LastModified"])) {
244
                $item["mtime"] = $file["LastModified"];
245
            }
246
            if (isset($file["Size"])) {
247
                $item["size"] = $file["Size"];
248
            }
249
            if (isset($file["Etag"])) {
250
                $item["etag"] = $file["Etag"];
251
            }
252
            $files[] = $item;
253
        }
254
255
        return $files;
256
    }
257
258
    /**
259
     * {@inheritdoc}
260
     */
261
    public function delete($key)
262
    {
263
        try {
264
            $this->service->deleteObject($this->getOptions($key));
265
266
            return true;
267
        } catch (\Exception $e) {
268
            return false;
269
        }
270
    }
271
272
    /**
273
     * {@inheritdoc}
274
     */
275
    public function isDirectory($key)
276
    {
277
        $result = $this->service->listObjects(array(
278
            'Bucket' => $this->bucket,
279
            'Prefix' => rtrim($this->computePath($key), '/').'/',
280
            'MaxKeys' => 1,
281
        ));
282
283
        return count($result['Contents']) > 0;
284
    }
285
286
    /**
287
     * Ensures the specified bucket exists. If the bucket does not exists
288
     * and the create option is set to true, it will try to create the
289
     * bucket. The bucket is created using the same region as the supplied
290
     * client object.
291
     *
292
     * @throws \RuntimeException if the bucket does not exists or could not be
293
     *                           created
294
     */
295
    protected function ensureBucketExists()
296
    {
297
        if ($this->bucketExists) {
298
            return true;
299
        }
300
301
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
302
            return true;
303
        }
304
305
        if (!$this->options['create']) {
306
            throw new \RuntimeException(sprintf(
307
                'The configured bucket "%s" does not exist.',
308
                $this->bucket
309
            ));
310
        }
311
312
        $options = array('Bucket' => $this->bucket);
313
        if ($this->service->getRegion() != 'us-east-1') {
314
            $options['LocationConstraint'] = $this->service->getRegion();
315
        }
316
317
        $this->service->createBucket($options);
318
        $this->bucketExists = true;
319
320
        return true;
321
    }
322
323
    protected function getOptions($key, array $options = array())
324
    {
325
        $options['ACL'] = $this->options['acl'];
326
        $options['Bucket'] = $this->bucket;
327
        $options['Key'] = $this->computePath($key);
328
329
        /*
330
         * Merge global options for adapter, which are set in the constructor, with metadata.
331
         * Metadata will override global options.
332
         */
333
        $options = array_merge($this->options, $options, $this->getMetadata($key));
334
335
        return $options;
336
    }
337
338 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...
339
    {
340
        if (empty($this->options['directory'])) {
341
            return $key;
342
        }
343
344
        return sprintf('%s/%s', $this->options['directory'], $key);
345
    }
346
347
    /**
348
     * Computes the key from the specified path.
349
     *
350
     * @param string $path
351
     *
352
     * return string
353
     */
354
    protected function computeKey($path)
355
    {
356
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
357
    }
358
}
359