1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Gaufrette\Adapter; |
4
|
|
|
|
5
|
|
|
use AsyncAws\SimpleS3\SimpleS3Client; |
6
|
|
|
use Gaufrette\Adapter; |
7
|
|
|
use Gaufrette\Util; |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* Amazon S3 adapter using the AsyncAws. |
11
|
|
|
* |
12
|
|
|
* @author Michael Dowling <[email protected]> |
13
|
|
|
* @author Tobias Nyholm <[email protected]> |
14
|
|
|
*/ |
15
|
|
|
class AsyncAwsS3 implements Adapter, MetadataSupporter, ListKeysAware, SizeCalculator, MimeTypeProvider |
16
|
|
|
{ |
17
|
|
|
/** @var SimpleS3Client */ |
18
|
|
|
protected $service; |
19
|
|
|
/** @var string */ |
20
|
|
|
protected $bucket; |
21
|
|
|
/** @var array */ |
22
|
|
|
protected $options; |
23
|
|
|
/** @var bool */ |
24
|
|
|
protected $bucketExists; |
25
|
|
|
/** @var array */ |
26
|
|
|
protected $metadata = []; |
27
|
|
|
/** @var bool */ |
28
|
|
|
protected $detectContentType; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* @param SimpleS3Client $service |
32
|
|
|
* @param string $bucket |
33
|
|
|
* @param array $options |
34
|
|
|
* @param bool $detectContentType |
35
|
|
|
*/ |
36
|
|
View Code Duplication |
public function __construct(SimpleS3Client $service, $bucket, array $options = [], $detectContentType = false) |
|
|
|
|
37
|
|
|
{ |
38
|
|
|
if (!class_exists(SimpleS3Client::class)) { |
39
|
|
|
throw new \LogicException('You need to install package "async-aws/simple-s3" to use this adapter'); |
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
|
|
|
* {@inheritdoc} |
57
|
|
|
*/ |
58
|
|
View Code Duplication |
public function setMetadata($key, $content) |
|
|
|
|
59
|
|
|
{ |
60
|
|
|
// BC with AmazonS3 adapter |
61
|
|
|
if (isset($content['contentType'])) { |
62
|
|
|
$content['ContentType'] = $content['contentType']; |
63
|
|
|
unset($content['contentType']); |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
$this->metadata[$key] = $content; |
67
|
|
|
} |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* {@inheritdoc} |
71
|
|
|
*/ |
72
|
|
|
public function getMetadata($key) |
73
|
|
|
{ |
74
|
|
|
return isset($this->metadata[$key]) ? $this->metadata[$key] : []; |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* {@inheritdoc} |
79
|
|
|
*/ |
80
|
|
View Code Duplication |
public function read($key) |
|
|
|
|
81
|
|
|
{ |
82
|
|
|
$this->ensureBucketExists(); |
83
|
|
|
$options = $this->getOptions($key); |
84
|
|
|
|
85
|
|
|
try { |
86
|
|
|
// Get remote object |
87
|
|
|
$object = $this->service->getObject($options); |
88
|
|
|
// If there's no metadata array set up for this object, set it up |
89
|
|
|
if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) { |
90
|
|
|
$this->metadata[$key] = []; |
91
|
|
|
} |
92
|
|
|
// Make remote ContentType metadata available locally |
93
|
|
|
$this->metadata[$key]['ContentType'] = $object->getContentType(); |
94
|
|
|
|
95
|
|
|
return $object->getBody()->getContentAsString(); |
96
|
|
|
} catch (\Exception $e) { |
97
|
|
|
return false; |
98
|
|
|
} |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* {@inheritdoc} |
103
|
|
|
*/ |
104
|
|
View Code Duplication |
public function rename($sourceKey, $targetKey) |
|
|
|
|
105
|
|
|
{ |
106
|
|
|
$this->ensureBucketExists(); |
107
|
|
|
$options = $this->getOptions( |
108
|
|
|
$targetKey, |
109
|
|
|
['CopySource' => $this->bucket . '/' . $this->computePath($sourceKey)] |
110
|
|
|
); |
111
|
|
|
|
112
|
|
|
try { |
113
|
|
|
$this->service->copyObject(array_merge($options, $this->getMetadata($targetKey))); |
114
|
|
|
|
115
|
|
|
return $this->delete($sourceKey); |
116
|
|
|
} catch (\Exception $e) { |
117
|
|
|
return false; |
118
|
|
|
} |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* {@inheritdoc} |
123
|
|
|
* @param string|resource $content |
124
|
|
|
*/ |
125
|
|
|
public function write($key, $content) |
126
|
|
|
{ |
127
|
|
|
$this->ensureBucketExists(); |
128
|
|
|
$options = $this->getOptions($key); |
129
|
|
|
unset($options['Bucket'], $options['Key']); |
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
|
|
View Code Duplication |
if (!isset($options['ContentType']) && $this->detectContentType) { |
|
|
|
|
136
|
|
|
$options['ContentType'] = $this->guessContentType($content); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
try { |
140
|
|
|
$this->service->upload($this->bucket, $this->computePath($key), $content, $options); |
141
|
|
|
|
142
|
|
|
if (is_resource($content)) { |
143
|
|
|
return (int) Util\Size::fromResource($content); |
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->has($this->bucket, $this->computePath($key)); |
158
|
|
|
} |
159
|
|
|
|
160
|
|
|
/** |
161
|
|
|
* {@inheritdoc} |
162
|
|
|
*/ |
163
|
|
View Code Duplication |
public function mtime($key) |
|
|
|
|
164
|
|
|
{ |
165
|
|
|
try { |
166
|
|
|
$result = $this->service->headObject($this->getOptions($key)); |
167
|
|
|
|
168
|
|
|
return $result->getLastModified()->getTimestamp(); |
169
|
|
|
} catch (\Exception $e) { |
170
|
|
|
return false; |
171
|
|
|
} |
172
|
|
|
} |
173
|
|
|
|
174
|
|
|
/** |
175
|
|
|
* {@inheritdoc} |
176
|
|
|
*/ |
177
|
|
|
public function size($key) |
178
|
|
|
{ |
179
|
|
|
$result = $this->service->headObject($this->getOptions($key)); |
180
|
|
|
|
181
|
|
|
return (int) $result->getContentLength(); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
public function mimeType($key) |
185
|
|
|
{ |
186
|
|
|
$result = $this->service->headObject($this->getOptions($key)); |
187
|
|
|
|
188
|
|
|
return $result->getContentType(); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* {@inheritdoc} |
193
|
|
|
*/ |
194
|
|
|
public function keys() |
195
|
|
|
{ |
196
|
|
|
return $this->listKeys(); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* {@inheritdoc} |
201
|
|
|
*/ |
202
|
|
View Code Duplication |
public function listKeys($prefix = '') |
|
|
|
|
203
|
|
|
{ |
204
|
|
|
$this->ensureBucketExists(); |
205
|
|
|
|
206
|
|
|
$options = ['Bucket' => $this->bucket]; |
207
|
|
|
if ((string) $prefix != '') { |
208
|
|
|
$options['Prefix'] = $this->computePath($prefix); |
209
|
|
|
} elseif (!empty($this->options['directory'])) { |
210
|
|
|
$options['Prefix'] = $this->options['directory']; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$keys = []; |
214
|
|
|
$result = $this->service->listObjectsV2($options); |
215
|
|
|
foreach ($result->getContents() as $file) { |
216
|
|
|
$keys[] = $this->computeKey($file->getKey()); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
return $keys; |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
/** |
223
|
|
|
* {@inheritdoc} |
224
|
|
|
*/ |
225
|
|
|
public function delete($key) |
226
|
|
|
{ |
227
|
|
|
try { |
228
|
|
|
$this->service->deleteObject($this->getOptions($key)); |
229
|
|
|
|
230
|
|
|
return true; |
231
|
|
|
} catch (\Exception $e) { |
232
|
|
|
return false; |
233
|
|
|
} |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* {@inheritdoc} |
238
|
|
|
*/ |
239
|
|
|
public function isDirectory($key) |
240
|
|
|
{ |
241
|
|
|
$result = $this->service->listObjectsV2([ |
242
|
|
|
'Bucket' => $this->bucket, |
243
|
|
|
'Prefix' => rtrim($this->computePath($key), '/') . '/', |
244
|
|
|
'MaxKeys' => 1, |
245
|
|
|
]); |
246
|
|
|
|
247
|
|
|
foreach ($result->getContents(true) as $file) { |
248
|
|
|
return true; |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
return false; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Ensures the specified bucket exists. If the bucket does not exists |
256
|
|
|
* and the create option is set to true, it will try to create the |
257
|
|
|
* bucket. The bucket is created using the same region as the supplied |
258
|
|
|
* client object. |
259
|
|
|
* |
260
|
|
|
* @throws \RuntimeException if the bucket does not exists or could not be |
261
|
|
|
* created |
262
|
|
|
*/ |
263
|
|
View Code Duplication |
protected function ensureBucketExists() |
|
|
|
|
264
|
|
|
{ |
265
|
|
|
if ($this->bucketExists) { |
266
|
|
|
return true; |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
if ($this->bucketExists = $this->service->bucketExists(['Bucket' => $this->bucket])->isSuccess()) { |
270
|
|
|
return true; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
if (!$this->options['create']) { |
274
|
|
|
throw new \RuntimeException(sprintf( |
275
|
|
|
'The configured bucket "%s" does not exist.', |
276
|
|
|
$this->bucket |
277
|
|
|
)); |
278
|
|
|
} |
279
|
|
|
|
280
|
|
|
$this->service->createBucket([ |
281
|
|
|
'Bucket' => $this->bucket, |
282
|
|
|
]); |
283
|
|
|
$this->bucketExists = true; |
284
|
|
|
|
285
|
|
|
return true; |
286
|
|
|
} |
287
|
|
|
|
288
|
|
View Code Duplication |
protected function getOptions($key, array $options = []) |
|
|
|
|
289
|
|
|
{ |
290
|
|
|
$options['ACL'] = $this->options['acl']; |
291
|
|
|
$options['Bucket'] = $this->bucket; |
292
|
|
|
$options['Key'] = $this->computePath($key); |
293
|
|
|
|
294
|
|
|
/* |
295
|
|
|
* Merge global options for adapter, which are set in the constructor, with metadata. |
296
|
|
|
* Metadata will override global options. |
297
|
|
|
*/ |
298
|
|
|
$options = array_merge($this->options, $options, $this->getMetadata($key)); |
299
|
|
|
|
300
|
|
|
return $options; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
protected function computePath($key) |
304
|
|
|
{ |
305
|
|
|
if (empty($this->options['directory'])) { |
306
|
|
|
return $key; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
return sprintf('%s/%s', $this->options['directory'], $key); |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Computes the key from the specified path. |
314
|
|
|
* |
315
|
|
|
* @param string $path |
316
|
|
|
* |
317
|
|
|
* return string |
318
|
|
|
*/ |
319
|
|
|
protected function computeKey($path) |
320
|
|
|
{ |
321
|
|
|
return ltrim(substr($path, strlen($this->options['directory'])), '/'); |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* @param string|resource $content |
326
|
|
|
* |
327
|
|
|
* @return string |
328
|
|
|
*/ |
329
|
|
View Code Duplication |
private function guessContentType($content) |
|
|
|
|
330
|
|
|
{ |
331
|
|
|
$fileInfo = new \finfo(FILEINFO_MIME_TYPE); |
332
|
|
|
|
333
|
|
|
if (is_resource($content)) { |
334
|
|
|
return $fileInfo->file(stream_get_meta_data($content)['uri']); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
return $fileInfo->buffer($content); |
338
|
|
|
} |
339
|
|
|
} |
340
|
|
|
|
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.