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
|
|||
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 |
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.