Completed
Push — master ( 24c8cf...091397 )
by Albin
02:18
created

src/Gaufrette/Adapter/AwsS3.php (1 issue)

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
    public function __construct(S3Client $service, $bucket, array $options = [], $detectContentType = false)
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(
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,
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);
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)
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)
208
    {
209
        try {
210
            $result = $this->service->headObject($this->getOptions($key));
211
212
            return $result['ContentLength'];
213
        } catch (\Exception $e) {
214
            return false;
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
        $options = ['Bucket' => $this->bucket];
232 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...
233
            $options['Prefix'] = $this->computePath($prefix);
234
        } elseif (!empty($this->options['directory'])) {
235
            $options['Prefix'] = $this->options['directory'];
236
        }
237
238
        $keys = [];
239
        $iter = $this->service->getIterator('ListObjects', $options);
240
        foreach ($iter as $file) {
241
            $keys[] = $this->computeKey($file['Key']);
242
        }
243
244
        return $keys;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function delete($key)
251
    {
252
        try {
253
            $this->service->deleteObject($this->getOptions($key));
254
255
            return true;
256
        } catch (\Exception $e) {
257
            return false;
258
        }
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264
    public function isDirectory($key)
265
    {
266
        $result = $this->service->listObjects([
267
            'Bucket' => $this->bucket,
268
            'Prefix' => rtrim($this->computePath($key), '/').'/',
269
            'MaxKeys' => 1,
270
        ]);
271
272
        return count($result['Contents']) > 0;
273
    }
274
275
    /**
276
     * Ensures the specified bucket exists. If the bucket does not exists
277
     * and the create option is set to true, it will try to create the
278
     * bucket. The bucket is created using the same region as the supplied
279
     * client object.
280
     *
281
     * @throws \RuntimeException if the bucket does not exists or could not be
282
     *                           created
283
     */
284
    protected function ensureBucketExists()
285
    {
286
        if ($this->bucketExists) {
287
            return true;
288
        }
289
290
        if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) {
291
            return true;
292
        }
293
294
        if (!$this->options['create']) {
295
            throw new \RuntimeException(sprintf(
296
                'The configured bucket "%s" does not exist.',
297
                $this->bucket
298
            ));
299
        }
300
301
        $this->service->createBucket([
302
            'Bucket' => $this->bucket,
303
            'LocationConstraint' => $this->service->getRegion()
304
        ]);
305
        $this->bucketExists = true;
306
307
        return true;
308
    }
309
310
    protected function getOptions($key, array $options = [])
311
    {
312
        $options['ACL'] = $this->options['acl'];
313
        $options['Bucket'] = $this->bucket;
314
        $options['Key'] = $this->computePath($key);
315
316
        /*
317
         * Merge global options for adapter, which are set in the constructor, with metadata.
318
         * Metadata will override global options.
319
         */
320
        $options = array_merge($this->options, $options, $this->getMetadata($key));
321
322
        return $options;
323
    }
324
325 View Code Duplication
    protected function computePath($key)
326
    {
327
        if (empty($this->options['directory'])) {
328
            return $key;
329
        }
330
331
        return sprintf('%s/%s', $this->options['directory'], $key);
332
    }
333
334
    /**
335
     * Computes the key from the specified path.
336
     *
337
     * @param string $path
338
     *
339
     * return string
340
     */
341
    protected function computeKey($path)
342
    {
343
        return ltrim(substr($path, strlen($this->options['directory'])), '/');
344
    }
345
346
    /**
347
     * @param string $content
348
     *
349
     * @return string
350
     */
351 View Code Duplication
    private function guessContentType($content)
352
    {
353
        $fileInfo = new \finfo(FILEINFO_MIME_TYPE);
354
355
        if (is_resource($content)) {
356
            return $fileInfo->file(stream_get_meta_data($content)['uri']);
357
        }
358
359
        return $fileInfo->buffer($content);
360
    }
361
362 View Code Duplication
    public function mimeType($key)
363
    {
364
        try {
365
            $result = $this->service->headObject($this->getOptions($key));
366
            return ($result['ContentType']);
367
        } catch (\Exception $e) {
368
            return false;
369
        }
370
    }
371
}
372