Completed
Pull Request — master (#421)
by Vincent
02:53
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
                       ListFilesAware,
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
19
                       SizeCalculator
0 ignored issues
show
Coding Style introduced by
Expected 4 spaces before interface name; 23 found
Loading history...
20
{
21
    protected $service;
22
    protected $bucket;
23
    protected $options;
24
    protected $bucketExists;
25
    protected $metadata = array();
26
    protected $detectContentType;
27
28 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...
29
    {
30
        $this->service = $service;
31
        $this->bucket = $bucket;
32
        $this->options = array_replace(
33
            array(
34
                'create' => false,
35
                'directory' => '',
36
                'acl' => 'private',
37
            ),
38
            $options
39
        );
40
41
        $this->detectContentType = $detectContentType;
42
    }
43
44
    /**
45
     * Gets the publicly accessible URL of an Amazon S3 object.
46
     *
47
     * @param string $key     Object key
48
     * @param array  $options Associative array of options used to buld the URL
49
     *                        - expires: The time at which the URL should expire
50
     *                        represented as a UNIX timestamp
51
     *                        - Any options available in the Amazon S3 GetObject
52
     *                        operation may be specified.
53
     *
54
     * @return string
55
     */
56
    public function getUrl($key, array $options = array())
57
    {
58
        return $this->service->getObjectUrl(
59
            $this->bucket,
60
            $this->computePath($key),
61
            isset($options['expires']) ? $options['expires'] : null,
62
            $options
63
        );
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function setMetadata($key, $metadata)
70
    {
71
        // BC with AmazonS3 adapter
72
        if (isset($metadata['contentType'])) {
73
            $metadata['ContentType'] = $metadata['contentType'];
74
            unset($metadata['contentType']);
75
        }
76
77
        $this->metadata[$key] = $metadata;
78
    }
79
80
    /**
81
     * {@inheritdoc}
82
     */
83
    public function getMetadata($key)
84
    {
85
        return isset($this->metadata[$key]) ? $this->metadata[$key] : array();
86
    }
87
88
    /**
89
     * {@inheritdoc}
90
     */
91
    public function read($key)
92
    {
93
        $this->ensureBucketExists();
94
        $options = $this->getOptions($key);
95
96
        try {
97
            return (string) $this->service->getObject($options)->get('Body');
98
        } catch (\Exception $e) {
99
            return false;
100
        }
101
    }
102
103
    /**
104
     * {@inheritdoc}
105
     */
106
    public function rename($sourceKey, $targetKey)
107
    {
108
        $this->ensureBucketExists();
109
        $options = $this->getOptions(
110
            $targetKey,
111
            array(
112
                'CopySource' => $this->bucket.'/'.$this->computePath($sourceKey),
113
            )
114
        );
115
116
        try {
117
            $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey)));
118
119
            return $this->delete($sourceKey);
120
        } catch (\Exception $e) {
121
            return false;
122
        }
123
    }
124
125
    /**
126
     * {@inheritdoc}
127
     */
128
    public function write($key, $content)
129
    {
130
        $this->ensureBucketExists();
131
        $options = $this->getOptions($key, array('Body' => $content));
132
133
        /*
134
         * If the ContentType was not already set in the metadata, then we autodetect
135
         * it to prevent everything being served up as binary/octet-stream.
136
         */
137
        if (!isset($options['ContentType']) && $this->detectContentType) {
138
            $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
139
            if (is_resource($content)) {
140
                $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']);
141
            } else {
142
                $contentType = $fileInfo->buffer($content);
143
            }
144
            $options['ContentType'] = $contentType;
145
        }
146
147
        try {
148
            $this->service->putObject($options);
149
            if (is_resource($content)) {
150
                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...
151
            } else {
152
                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...
153
            }
154
        } catch (\Exception $e) {
155
            return false;
156
        }
157
    }
158
159
    /**
160
     * {@inheritdoc}
161
     */
162
    public function exists($key)
163
    {
164
        return $this->service->doesObjectExist($this->bucket, $this->computePath($key));
165
    }
166
167
    /**
168
     * {@inheritdoc}
169
     */
170 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...
171
    {
172
        try {
173
            $result = $this->service->headObject($this->getOptions($key));
174
175
            return strtotime($result['LastModified']);
176
        } catch (\Exception $e) {
177
            return false;
178
        }
179
    }
180
181
    /**
182
     * {@inheritdoc}
183
     */
184 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...
185
    {
186
        try {
187
            $result = $this->service->headObject($this->getOptions($key));
188
189
            return $result['ContentLength'];
190
        } catch (\Exception $e) {
191
            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...
192
        }
193
    }
194
195
    /**
196
     * {@inheritdoc}
197
     */
198
    public function keys()
199
    {
200
        return $this->listKeys();
201
    }
202
203
    /**
204
     * {@inheritdoc}
205
     */
206
    public function listKeys($prefix = '')
207
    {
208
        $options = array('Bucket' => $this->bucket);
209 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...
210
            $options['Prefix'] = $this->computePath($prefix);
211
        } elseif (!empty($this->options['directory'])) {
212
            $options['Prefix'] = $this->options['directory'];
213
        }
214
215
        $keys = array();
216
        $iter = $this->service->getIterator('ListObjects', $options);
217
        foreach ($iter as $file) {
218
            $keys[] = $this->computeKey($file['Key']);
219
        }
220
221
        return $keys;
222
    }
223
224
    /**
225
     * List files beginning with given prefix.
226
     * Similar to listKeys but this will also return file mtime, size, and etag.
227
     * @param string $prefix
228
     * @return mixed
229
     */
230
    public function listFiles($prefix = '')
231
    {
232
        $options = array('Bucket' => $this->bucket);
233 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...
234
            $options['Prefix'] = $this->computePath($prefix);
235
        } elseif (!empty($this->options['directory'])) {
236
            $options['Prefix'] = $this->options['directory'];
237
        }
238
239
        $files = array();
240
        $iter = $this->service->getIterator('ListObjects', $options);
241
        foreach ($iter as $file) {
242
            $item= [];
243
            $item["key"] = $this->computeKey($file['Key']);
244
            if (isset($file["LastModified"])) {
245
                $item["mtime"] = $file["LastModified"];
246
            }
247
            if (isset($file["Size"])) {
248
                $item["size"] = $file["Size"];
249
            }
250
            if (isset($file["Etag"])) {
251
                $item["etag"] = $file["Etag"];
252
            }
253
            $files[] = $item;
254
        }
255
256
        return $files;
257
    }
258
259
    /**
260
     * {@inheritdoc}
261
     */
262
    public function delete($key)
263
    {
264
        try {
265
            $this->service->deleteObject($this->getOptions($key));
266
267
            return true;
268
        } catch (\Exception $e) {
269
            return false;
270
        }
271
    }
272
273
    /**
274
     * {@inheritdoc}
275
     */
276
    public function isDirectory($key)
277
    {
278
        $result = $this->service->listObjects(array(
279
            'Bucket' => $this->bucket,
280
            'Prefix' => rtrim($this->computePath($key), '/').'/',
281
            'MaxKeys' => 1,
282
        ));
283
284
        return count($result['Contents']) > 0;
285
    }
286
287
    /**
288
     * Ensures the specified bucket exists. If the bucket does not exists
289
     * and the create option is set to true, it will try to create the
290
     * bucket. The bucket is created using the same region as the supplied
291
     * client object.
292
     *
293
     * @throws \RuntimeException if the bucket does not exists or could not be
294
     *                           created
295
     */
296
    protected function ensureBucketExists()
297
    {
298
        if ($this->bucketExists) {
299
            return true;
300
        }
301
302
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
303
            return true;
304
        }
305
306
        if (!$this->options['create']) {
307
            throw new \RuntimeException(sprintf(
308
                'The configured bucket "%s" does not exist.',
309
                $this->bucket
310
            ));
311
        }
312
313
        $options = array('Bucket' => $this->bucket);
314
        if ($this->service->getRegion() != 'us-east-1') {
315
            $options['LocationConstraint'] = $this->service->getRegion();
316
        }
317
318
        $this->service->createBucket($options);
319
        $this->bucketExists = true;
320
321
        return true;
322
    }
323
324
    protected function getOptions($key, array $options = array())
325
    {
326
        $options['ACL'] = $this->options['acl'];
327
        $options['Bucket'] = $this->bucket;
328
        $options['Key'] = $this->computePath($key);
329
330
        /*
331
         * Merge global options for adapter, which are set in the constructor, with metadata.
332
         * Metadata will override global options.
333
         */
334
        $options = array_merge($this->options, $options, $this->getMetadata($key));
335
336
        return $options;
337
    }
338
339 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...
340
    {
341
        if (empty($this->options['directory'])) {
342
            return $key;
343
        }
344
345
        return sprintf('%s/%s', $this->options['directory'], $key);
346
    }
347
348
    /**
349
     * Computes the key from the specified path.
350
     *
351
     * @param string $path
352
     *
353
     * return string
354
     */
355
    protected function computeKey($path)
356
    {
357
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
358
    }
359
}
360